Using Redux with Ducks allows you to minimize the amount of files needed to organize the different parts that redux needs in order to operate. With ducks you only need 1 parent folder "Modules".
Within the modules folder exists an index.js file and 1module
file for each element to be stored.
We will create a counter.js file within Modules to work on
The first thing to do is define the different action types that will be used with the stored Element
For example within a simple couter file there will be two actions: Increase and Decrease
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
Since the name INCREASE or DECREASE can be used again in other modules, we include
filename/
infront in order to make the action type unique. Since each file will have only one action of the same name, this naming technique prevents collision.
Action functions have to be created using the action types created above. Action types define what kind of action the action function will perform. Action functions can take in a parameter that contains information that can be returned along with the action type.
export const increase = () => ({ type: INCREASE});
export const decrease = () => ({ type: DECREASE});
Below is an example containing a payload
let id = 3;
export const insert = (text) => ({
type: INSERT,
payload: {
id: id++,
text,
done:false
}
});
Data stored in local variables(payload.id) and preset data(payload.done) can be passed along with the parameter
First install in terminal
$ yarn add redux-actions
or
$ npm install --save redux-actions
Import createAction from redux-actions
import { createAction } from 'redux-actions';
Using createAction eliminates the need to create an object that contains type and payload
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
Below is an example containing a payload
let id = 3;
export const insert = createAction(INSERT,(text)=>({
id: id++,
text,
done:false
});
*** when the parameter is passed along without change ***
export const changeInput = createAction(CHANGE_INPUT, input => input);
Creating an intialState is very simple. Just define a const variable containing the state you need.
const initialState = {
number: 0
};
So how do you create a reducer function? The reducer function in ducks looks exactly the same.
function counter(state = initialState, action){
switch () {
case INCREASE:
return {
number: state.number + 1
};
case DECREASE:
return {
number: state.number - 1
};
default:
return state;
}
}
export default counter;
Import handleActions from redux-actions
import { createAction, handleActions } from 'redux-actions';
handleActions
is used to make the reducer function easier to read by eliminating the need to use the switch case.
const counter = handleActions(
{
[INCREASE]: (state, action) => ({ number: state.number + 1});
[DECREASE]: (state, action) => ({ number: state.number - 1});
},
initialState,
);
export default counter;
Additional tips to increase readability
const todos = handleActions(
{
[CHANGE_INPUT]: (state, action) => ({...state, input: action.payload}),
[INSERT]: (state, action) => ({...state, todos: action.payload})
},
initialState,
);
In the case above, since all the payloads have the same name of action.payload, what the payload is refering to can get confusing.
const todos = handleActions(
{
[CHANGE_INPUT]: (state, {payload:input}) => ({...state, input}),
[INSERT]: (state, {payload:todo}) => ({...state, todo})
},
initialState,
);
By using destructuring assignment you can replace state.payload with an element name that is substantially more comprehensible.
When updating States through reducers, spread operators are often used. In other cases the
map
function andfilter
function is used to return a new element. However when a piece of data stored deep inside of anobject
needs to be accessed, theproduce
function from theimmer
library can be used.
import { produce } from 'immer';
*** using map function ***
[TOGGLE]: (state, { payload: id }) => ({
...state,
todos: state.todos.map(todo =>
todo.id === id ? {...todo, dont: !todo.done} : todo
)
})
*** using produce ***
[TOGGLE]: (state, { payload: id }) =>
produce(state, draft => {
const index = draft.todos.find(todo => todo.id === id);
todo.done = !todo.done;
})
import counter from './counter';
import { increase, decrease } from './counter';
or
import counter, { increase, decrease } from './counter';
Using Hooks allows you to eliminate the need to use connect, mapstatetoprops, etc. This makes the use of actions and store substantially simpler.
useSelector
You can acess states stored in Redux without having to use the connect function with
useSelector
import { useSelector, useDispatch } from 'react-redux';
import { increase, decrease } from './modules/counter';
const number = useSelector(state=>state.counter.number);
useDispatch
allows you to activate the imported actions easily
import { useSelector, useDispatch } from 'react-redux';
import { increase, decrease } from './modules/counter';
const number = useSelector(state=>state.counter.number);
const onIncrease = () => dispatch(increase());
const onDecrease = () => dispatch(decrease());
When using the connect function, even if the parent component rerenders, if the props of the currrent component doesn't change, the component doesnt rerender. However, when using the useSelector Hooks this isn't applied. Thus use React.memo for performance optimization.
import React from 'react';
(...)
export default React.memo(TodosContainer);
import { createAction, handleActions } from "redux-actions";
let listCNT = 4;
let cardCNT = 10;
//Action variables
const ADD_Card = "lists/ADD_Card";
const ADD_LIST = "lists/ADD_LIST";
const DRAG_HAPPENED = "lists/DRAG_HAPPENED";
//Actions
export const addCard = createAction(ADD_Card, (listID, text) => ({
text,
listID,
}));
export const addList = createAction(ADD_LIST);
export const sort = createAction(
DRAG_HAPPENED,
(
droppableIdStart,
droppableIdEnd,
droppableIndexStart,
droppableIndexEnd,
draggableId,
type
) => ({
droppableIdStart,
droppableIdEnd,
droppableIndexStart,
droppableIndexEnd,
draggableId,
type,
})
);
//Reducer
const initialState = [
{
title: "RFQ",
id: `list-${0}`,
cards: [
{
id: `card-${0}`,
text: "we created 1st card",
},
{
id: `card-${1}`,
text: "we created 2nd card",
},
],
},
{
title: "OFFER",
id: `list-${1}`,
cards: [
{
id: `card-${2}`,
text: "this is new one",
},
{
id: `card-${3}`,
text: "everything is going good, right?",
},
{
id: `card-${4}`,
text: "We will also make some change",
},
{
id: `card-${5}`,
text: "Trade Force, Vamos",
},
],
},
{
title: "ORDER",
id: `list-${2}`,
cards: [
{
id: `card-${6}`,
text: "we created 1st card",
},
{
id: `card-${7}`,
text: "we created 2nd card",
},
],
},
{
title: "SHIPMENT",
id: `list-${3}`,
cards: [
{
id: `card-${8}`,
text: "멋쟁이 아들",
},
{
id: `card-${9}`,
text: "we created 2nd card",
},
],
},
];
const listsReducers = handleActions(
{
[ADD_Card]: (state, { payload: cardinfo }) => {
const newState = state.map((list) => {
if (list.id === cardinfo.listID) {
const newCard = {
text: cardinfo.text,
id: `card-${cardCNT}`,
};
cardCNT++;
return {
...list,
cards: [...list.cards, newCard],
};
} else {
return list;
}
});
return newState;
},
[ADD_LIST]: (state, { payload: listinfo }) => {
const newList = { title: listinfo, cards: [], id: `list-${listCNT}` };
listCNT++;
return [...state, newList];
},
[DRAG_HAPPENED]: (state, { payload: draginfo }) => {
const {
droppableIdStart,
droppableIdEnd,
droppableIndexStart,
droppableIndexEnd,
draggableId,
type,
} = draginfo;
const newState = [...state];
// dragging lists around
if (type === "list") {
const list = newState.splice(droppableIndexStart, 1);
newState.splice(droppableIndexEnd, 0, ...list);
return newState;
}
// In the same list
if (droppableIdStart === droppableIdEnd) {
const list = state.find((list) => droppableIdStart === list.id);
const card = list.cards.splice(droppableIndexStart, 1);
list.cards.splice(droppableIndexEnd, 0, ...card);
}
// other list
if (droppableIdStart !== droppableIdEnd) {
// find the list where drag happened
const listStart = state.find((list) => droppableIdStart === list.id);
// pull out the card from this list
const card = listStart.cards.splice(droppableIndexStart, 1);
// find the list where drag ended
const listEnd = state.find((list) => droppableIdEnd === list.id);
// put the card in the new list
listEnd.cards.splice(droppableIndexEnd, 0, ...card);
}
return newState;
},
},
initialState
);
export default listsReducers;