I have been using Redux for a while, but I still can't see the point of actions and reducers.
As described in the docs, a reducer can be summarised as (previousState, action) => newState. The same principle applies for React's useReducer.
So this reducer function basically handles all actions, which seems like a violation of the Single Responsibility Principle. I'm sure that there's a good reason to do it this way, but I don't see it.
It would make more sense to me to just have a function per action. So instead of having an ADD_TODO action you would have a addTodo(previousState, todoText) => newState function. This would reduce (no pun intended) a lot of boilerplate code and might even give a slight performance improvement as you no longer need to switch through the action types.
So my question is: What's the advantage of having a reducer as opposed to a single-action function?
So if your question is why do we use reducers at all?
The real boilerplate of Flux is conceptual: the need to emit an
update, the need to register the Store with a Dispatcher, the need for
the Store to be an object (and the complications that arise when you
want a universal app).
That is a fundamental design choice redux has made as it is inspired from flux.
If you do not like the switch cases, and there by the reducer size. You can have something like this below:
export const todos = createReducer([], {
[ActionTypes.ADD_TODO]: (state, action) => {
const text = action.text.trim()
return [...state, text]
}
})
Above is a function that lets us express reducers as an object mapping from action types to handlers.
createReducer can be defined as :
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
You can read more on this here
Actually it is a silly design and makes no sense.
It is just a kind of over-engineering. Because in most case, you could have just used a simpler function to update a state instead of doing it a silly way by making it two functions (reducer and action).
By adopting redux in your project, you may write tons of reducer and action codes which looks extremely ugly and difficult to do code reviews
Try to imagine, if useState function is design by redux way!
const [todos, setTodos] = useState([])
this one line code now would looks like this(tons of ugly codes):
export interface TODO {
name: string
}
export interface ITodosAction {
type:
| "SET_TODOS";
payload: {
todos?: TODO[];
};
}
export default function todosReducer(
state: TODO[] = [],
action: ITodosAction
): TODO[] {
export function setTodos(todos: TODO[]): ITodosAction {
return {
type: "SET_TODOS",
payload: { todos }
};
}
const todos = useSelector(state=>state.todos)
dispatch(setTodos(["A", "B"]));
if useState became like this, we would have to suffer using react (thanks to react's team for not adopting an over-engineering and silly design like redux).
Remember: never try to figure out why this design is good when it has already brought you trouble. We use redux not because redux is good (instead it is a worst design i've ever seen) but because we don't have many choices
I think you're not appreciating how flexible reducers are, you can create as many of them as you like:
const initialState = {
key: 'someValue',
key2: 'someValue',
key3: {
key31: 'someValue',
key32: 'someValue',
key33: []
}
}
// all the below reducers can live in separate files and only handle a single action, if they want, no switches involved at all
const key = (state = initialState.key, action) => { /* the logic to be performed at 'key' */ }
const key2 = (state = initialState.key, action) => { /* the logic to be performed at 'key2' */ }
const key31 = (state = initialState.key3.key31, action) => { /* the logic to be performed at 'key3.key31' */ }
const key32 = (state = initialState.key3.key32, action) => { /* the logic to be performed at 'key3.key32' */ }
const key33 = (state = initialState.key3.key33, action) => { /* the logic to be performed at 'key3.key33' */ }
const key3 = combineReducers({key31,key32,key33})
// this is the top level reducer for your store
const myTopLevelReducer = combineReducers({key,key2,key3});
Related
I have a larger reducer, and I want to add another slice to it that will be fairly complicated unto itself. For example, here's a large, already complex reducer:
const initialState = {
bigchunk1: { ...someObject },
bigchunk2: true,
bigchunk3: [...somevalues],
bigchunk4: 'etc'
}
function mainReducer(state = initialState, action){
switch(action.type){
case Actions.CASE1:
return {
...state,
bigchunk1: {
...state.bigchunk1;
somevalue: action.payload.this_is_already_enough_nesting
}
}
case Actions.CASE2:
return {
...state,
bigchunk4: 'this goes on for awhile'
}
// lots more cases
default:
return state
}
}
I want to add a new slice to state, so the whole initialState would end up in the store looking like this:
const initialState = {
bigchunk1: { ...someObject },
bigchunk2: true,
bigchunk3: [...somevalues],
bigchunk4: 'etc',
newslice: { ...someComplicatedThing }
}
But rather than have to write all my new cases into my original reducer (along with a very messy level of nesting and spread operators), I want to write a new reducer that handles just the cases pertinent to newslice:
const initialState = { ...someComplicatedThing }
function newSliceReducer(state = initialState, action){
switch(action.type}{
// cases here
}
}
I am aware of combineReducers, but I'm having a hard time thinking of how to apply that in this scenario. combineReducers can take these two reducers and place them as siblings, but how can I combine these so that newSliceReducer and its associated state becomes a child of mainReducer under the name newslice? I feel like this should be simple, but its escaping me right now. I have been reading the redux docs, but I'm not seeing the answer clearly. I do not want to use redux-toolkit or any outside libraries.
In the default switch case, you can do
default:
let newSlice = newSliceReducer(state.newslice, action)
if (newSlice !== state.newslice) {
return {...state,newslice:newSlice}
}
return state
You can think of Redux as a coding pattern instead of library.
A reducer's job is to update the state and return new object if changed else return old object. If a new object is returned, react assumes something changed and a refresh is triggered.
Now, what you have is a "newSliceReducer" whose state is just a part of main reducer. So, I followed above rules and called the new reducer with state as smaller part of the original state and checked the result if it changed. If yes, I created a new object with updated state else return old state.
It is important to check if newSlice actually changed.
According to the Redux documentation, it seems to be a standard practice to set an initialState on your reducer. However this initialState needs to be maintained and if the state is being populated based on an API response, then you may have the initial state out of sync with the API response. This is especially true in cases where the state is made up of nested objects.
Is it to avoid null-checking (sometimes the initial state is set to null), are there any performance benefits? Does it improve code readability?
Taken from Redux docs:
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
Then in our reducer we may have an action which replaces the value in the state (for example based on an API response). Such as:
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_TODOS:
return Object.assign({}, state, {
todos: action.todos
})
default:
return state
}
}
However the same behaviour may be achieved without using an initialState, by checking the state in the component (or selector).
const MyComponent = ({todos}) => {
if (!todos) { // if we do not have an initialState, todos will be undefined if SET_TODOS hasn't been called
return null;
}
return <div>{todos.map(n => ...)}</div>
}
If the API returns a new property (notes), we would need to update as follows:
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: [],
notes: [] // <-----
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_TODOS:
return Object.assign({}, state, {
todos: action.todos,
})
case SET_NOTES:
return Object.assign({}, state, {
notes: action.notes,
})
default:
return state
}
}
This is further complicated in cases when you have nested objects. If the todos has a child property subtasks: [], why are we not setting an initial state for it?
I think it's more of a convenience than anything else. I highly doubt there is any performance implications. Also while in my personal opinion it is cleaner to add all the properties you expect upfront, you are not really required to add it to the initial state. You can simply add it when returning the new state, obviously if you're using typescript this is a different story.
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
// notes: [] you don't necessarily need to add it to the initial state
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_TODOS:
return Object.assign({}, state, {
todos: action.todos,
notes: action.notes // <-----
})
default:
return state
}
}
Having an initial state helps to have "cleaner" code. Like Brian Thompson said in the comments, code is more predictable if your data structure stays consistent.
It is also recommended (by some ppl) to avoid having multiples returns in one function. It may not be the best perf-wise, but it might be the easiest to read.
So having to do an early return in your component might not be the cleanest way to achieve the behavior your want.
That being said, if your implementation works, why not use it ? Well i think it's best when working as a team to stick to the conventions as much as possible.
I'm building a library that other apps will use. My library is an extension to redux.
To keep this question as general as possible, I currently have middlewares, action creators and one reducer.
The reducer is the problem because my reducer depends on the state structure which I, as a library developer, can't and shouldn't know. The user may use a combineReducers function or other function and give my reducer's state any name he wants.
My question is - What options Redux.js library provide to library developers in a case like this; hiding reducers/other alternatives to reducers?
Wrap your library in a configuration function, which requires the user to supply a selector, that points to place in the state that your reducer occupies.
In addition, if users access your state (not the case for you right now), you can supply selectors to use the state, without knowing it's structure.
A general non-working example:
const createSelectors = (mainSelector) => ({
selectorA: (state) => mainSelector(state).dataA,
selectorB: (state) => mainSelector(state).dataB,
});
const createMiddlewares = (actionTypes, selectors) => {
const middlewareA = ({ getState, dispatch }) =>
next => action => {
const myStateA = selectors.selectorA(getState());
};
return {
middlewareA
};
};
const factory = (mainSelector = ({ myState }) => myState) => {
const actionTypes = {};
const actions = {};
const reducer = () => {};
const selectors = createSelectors(mainSelector);
const middleware = createMiddlewares(actionTypes, selectors);
return {
actionTypes,
actions,
reducer,
middleware,
selectors
};
};
This package minimize needs for writing reducers, because it is using auto merge feature with actions. There is some solution with arrays too.
https://www.npmjs.com/package/micro-reducers
I think instead of struggling with the complex Redux library. you should give it a try with https://rootzjs.org/ an alternative to redux. Its literally like a cakewalk.
I'm using React and Redux in my web app.
In the login page, I have multiple fields (inputs).
The login page in composed from multiple components to pass the props to.
I was wondering how should I pass the props and update actions.
For example, lets assume I have 5 inputs in my login page.
LoginPage (container) -> AuthenticationForm (Component) -> SignupForm (Component)
In the LoginPage I map the state and dispatch to props,
and I see 2 options here:
mapStateToProps = (state) => ({
input1: state.input1,
...
input5: state.input5
})
mapDispatchToProps = (dispatch) => ({
changeInput1: (ev) => dispatch(updateInput1(ev.target.value))
...
changeInput5: (ev) => dispatch(updateInput5(ev.target.value))
})
In this solution, I need to pass a lot of props down the path (the dispatch actions and the state data).
Another way to do it is like this:
mapStateToProps = (state) => ({
values: {input1: state.input1, ..., input5: state.input5}
})
mapDispatchToProps = (dispatch) => ({
update: (name) => (ev) => dispatch(update(name, ev.target.value))
})
In this solution, I have to keep track and send the input name I want to update.
How should I engage this problem?
It seems like fundamental question, since a lot of forms have to handle it,
but I couldn't decide yet what would suit me now and for the long run.
What are the best practices?
I think best practice would be to handle all of this logic in the React component itself. You can use component's state to store input's data and use class methods to handle it. There is good explanation in React docs https://reactjs.org/docs/forms.html
You probably should pass data in Redux on submit. Ether storing whole state of the form as an object, or not store at all and just dispatching action with api call.
TL;DR. it's a more 'general' coding practice. But let's put it under a react-redux context.
Say if you go with your first approach, then you will probably have 5 actionCreators as:
function updateInput1({value}) { return {type: 'UPDATE_INPUT1', payload: {value}} }
...
function updateInput5({value}) { return {type: 'UPDATE_INPUT5', payload: {value}} }
Also if you have actionTypes, then:
const UPDATE_INPUT1 = 'UPDATE_INPUT1'
...
const UPDATE_INPUT5 = 'UPDATE_INPUT5'
The reducer will probably look like:
function handleInputUpdate(state = {}, {type, payload: {value}}) {
switch (type) {
case UPDATE_INPUT1: return {..., input1: value}
...
case UPDATE_INPUT5: return {..., input5: value}
default: return state
}
}
What's the problem? I don't think you're spreading too many props in mapStateToProps/mapDispatchToProps, Don't repeat yourself!
So naturally, you want a more generic function to avoid that:
const UPDATE_INPUT = 'UPDATE_INPUT'
function updateInput({name, value}) { return {type: UPDATE_INPUT, payload: {name, value}} }
function handleInputUpdate(state = {inputs: null}, {type, payload: {name, value}}) {
switch (type) {
case UPDATE_INPUT: return {inputs: {...state.inputs, [name]: value}}
default: return state
}
}
Finally, the "selector" part, based upon how the state was designed, get component's props from it would be fairly trivial:
function mapStateToProps(state) { return {inputs: state.inputs} }
function mapDispatchToProps(dispatch) { return {update(name, value) { dispatch(updateInput(name, value)) } }
In summary, it's not necessarily a redux/react problem, it's more how you design app state, redux just offers you utilities and poses some constraints to enable "time traveling" (state transitions are made explicit within a mutation handler based on a separate action).
Best practice to handle this problem is having a local state on your Form Component and managing it locally because I believe it's not a shared state. onSubmit you could dispatch your action passing down the state to the action which is required in making an API call or posting it to your server.
If you try to keep updating your store as the user types, it will keep dispatching the action which might cause problems in future. You read more here Handling multiple form inputs in react
I have a component which builds onto the Select component from Ant Design https://ant.design/components/select/
<SomeComponent
onSelect = { this.props.handleSelect }
onDeselect = { this.props.handleDeselect }
selectionList = { valuesList }
value = { values }/>
onSelect triggeres the action this.props.handleSelect
export function handleSelect(value) {
return dispatch => {
dispatch(actionCreator(HANDLE_SELECT, value));
}
}
That actions goes into the reducer
case HANDLE_SELECT: {
const newValues = value_select(state, action);
return {
...state,
find: {
...state.a,
values: newValues
}
}
}
Finally, value_select is called to do all the magic
export const value_select = function(state, action) {
...
const newData = {
XYZ: action.payload
}
return newData
}
This brings me to my question.
Is it possible to send further metadata with the action? Imagine I use the component <SomeComponent.../> several times. I would not know which of the rendered components triggered the action when the onSelect is fired.
If I want to process the information in value_select = function(state, action) {... later, I want to know which component caused the action to process my data properly. I need to set XYZ in value_select() dynamically, depending on which <SomeComponent.../> caused the action. action.payload only gives me what is saved in value in <SomeComponent.../>, nothing more.
Is there a way to send some more information with the onSelect or is that bad practice and I would need an action for each component <SomeComponent.../> anyway?
Absolutely. It's your action and your reducer, you can attach any information you want to it.
The most common approach for structuring an action is the Flux Standard Action approach, which expects your actions to look like {type, payload, meta, error} but it's really up to you what you put into your actions.
For some more ideas, you might want to read through the Structuring Reducers - Reusing Reducer Logic section of the Redux docs.