Bad idea to put a dom operation inside a redux reducer? - javascript

I have several actions which use the same reducer, and instead of having a dom operation in each of those actions, I want to just add it once inside my shared reducer. I know reducers are to be pure (which the returned data still is), but is this some kind of anti-pattern or an acceptable strategy?
case APPEND_POSTS:
!payload.length &&
document.getElementById('posts-cont').classList.add('no-more-posts'); // this
const total = state.posts.length + payload.length;
const limit = total > posts_to_keep_limit ? 50 : 0;
return {
...state,
posts: [...state.posts.slice(limit), ...payload],
loading: false,
};
```

Redux Action
case APPEND_POSTS:
// you don't need to use below code.
// !payload.length && document.getElementById('posts-cont').classList.add('no-more-posts'); // this
const total = state.posts.length + payload.length;
const limit = total > posts_to_keep_limit ? 50 : 0;
return {
...state,
posts: [...state.posts.slice(limit), ...payload],
nomore: true,
loading: false,
};
Your component.
function YourComp(props){
const state = useSelector(...);
return ( <div id="posts-cont" className={state.nomore ? 'no-more-posts' : ''} > {...}</div>
}

I know reducers are to be pure (which the returned data still is), but is this some kind of anti-pattern or an acceptable strategy?
The returned data is pure, but you've introduced a side-effect in the form of a DOM mutation. Therefore, this reducer is not pure.
This is indeed an anti-pattern because now, the component(s) that render posts-cont items have an invisible coupling to this reducer. It makes your codebase more difficult to read and debug.

jinongun's advice is good: let the className of the component derive its value from the store's state using a selector. AS for the general question
I have several actions which use the same reducer, and instead of
having a dom operation in each of those actions, I want to just add it
once inside my shared reducer.
DON'T EVER make DOM operations inside a reducer.
Don't ever make any operation that is not a pure computation.
But you can create an action creator that always calls a side effect (with Redux-Thunk):
function appendPosts(payload) {
return dispatch => {
mySideEffect()
dispatch({
type: APPEND_POSTS,
payload
})
}
}
function action1(params) {
return dispatch => {
dispatch({
type: ACTION1,
payload: params
})
dispatch(appendPosts(params))
}
}
function action2(params) {
return dispatch => {
dispatch({
type: ACTION2,
payload: params
})
dispatch(appendPosts(params))
}
}
// etc

Related

How to check action payload before state update?

I'm learning redux for my first react-redux application. How do I manage to verify payload value before changing my state ? For example the code below:
todoExample = {name: 'learn redux', author: 'myself'}
wrongTodoExample = {name: 'learn redux'}
dispatch(addTodos({todo: todoExample}))
dispatch(addTodos({todo: wrongTodoExample }))
With the above code, I add 2 todo items to my state but they don't have the same keys.
Is there a way to check the payload value in order to authorize the first addTodos but not the second one in my reducer?
I've searched on the internet but I couldn't find an answer. I'm sorry if my question is redundant.
You can use redux middleware to verify things, that is absolutely one of the intended use cases for middleware. Any middleware can inspect and modify any action going through the pipeline before it reaches the reducers, and even prevent an action from continuing on.
const verifyPayload = store => next => action => {
if (isVerifyPayload(action.payload)) {
return next(action);
} else {
return store.dispatch({ type: 'NOT_AUTHORIZED' })
}
}
const store = createStore(
initialState,
applyMiddleware(verifyPayload)
)
Not so clear about your description about same key, you mean name or author, or other specific keys like code\id.
You can try to validate your todos before dispatch or within the addTodos
function addTodos(payload) {
if (!payload.todo.code) return;
// simply return,
// otherwise throw an error to indicate that your todos miss a specific key
}
You can use a ternary operator in your reducer along with some util function to validate your todo. If the todo is valid, then transform your state to include the new todo, if not return the same state (effectively doing nothing).
const isValidTodo = (todo) => {
//Implement your validations. E.g: A valid todo will have a name and an author
return todo.name && todo.author;
}
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return isValidTodo(action.payload) ?
[
...state,
{
name: action.payload.name,
author: action.payload.text,
completed: false
}
]
: state
default:
return state
}
}
I've found a solution that suited well my needs and it's TypeScript. Now I have Payload Type wich allow me to define keys that I need in my action.payload without any validation function.
Thanks all for your asnwers.

Why Redux returns old state when dispatch action before?

I want to know why when I dispatch action before my console log prints old state.
if I do next:
reducer.js
let initialState = { display: false };
const MyReducer = (state = initialState,action) => {
...
case 'SET_DISPLAY':
return { update(state,{ display : {$set: action.display } }) }
break;
default:
return state;
break;
}
ActionCreator.js
let ActionCreator = {
setDisplay(value) {
return(dispatch,getState) {
dispatch({ type: 'SET_DISPLAY',display: value})
}
}
};
app.js
componentDidMount(){
this.props.dispatch(ActionCreator.setDisplay(true))
// expected : true
console.log(this.props.display)
// prints : false.
}
const mapStateToProps = (state) => {
display : state.display
}
but I can see changes in my redux dev-tools console.
PD I use redux-thunk as Middleware.its just example,all my code seems good and works great,but,its a question.
Why console logs old state instead a new state (its ilogic, if I dispatched an action before call logs) I will apreciate your answers,thanks.
This is because you are using redux-thunk and your dispatch happens aynchronously.
this.props.dispatch(ActionCreator.setDisplay(true)) will not set display true immediately.
Since you are not making a network request or anything async in that action why dont you change the action creator to
let ActionCreator = {
setDisplay(value) {
return { type: 'SET_DISPLAY',display: value};
}
};
Now it will happen synchronously. Also dont put console log immediately after dispatching. As redux updates state, old state is not modified. Instead it creates a new state instance with updated value. This new value will be passed as props to your component via connect of react-redux.
Try printing display in render() method, you will see that it is called twice and second one will display true.
First, I would recommend not to rely on the fact that dispatching an action may be synchronous; design as if everything was asynchronous. When eventually you dispatch an async actions, you will be pleased to have your mindset ready for that.
Second, your action creator return a function (you must be using the thunk middleware), which is why you get this behaviour.
componentDidMount(){
startSomethingAsync();
}
componentDidUpdate(){
if (!this.props.asyncCompleted) return;
if(this.props.asyncResultFn) {
this.props.dispatch({ type: ... value: VALUE_CONDITIONAL_TRUE})
}
else{
this.props.dispatch({ type: ... value: VALUE_CONDITIONAL_FALSE})
}
}

Complex state modifications on a reducer

I'm sending from the view to an action this.props.appClasses This is the view:
<div key={key} className='SDWanAppClassTreeItem' onClick={this.props.actions.handleToggleAppClass.bind(this,this.props.appClasses, 0, key)}>
In the action I modify appClasses that I get from the view, I want to send appClasses modified to the reducer to update the appClasses state. But it gives me an error before reach the reducer.
A state mutation was detected
This is the action:
export function handleToggleAppClass(appClasses, parentAppClassId, appClassId) {
// console.log('handleToggleAppClass', appClassId, this.appClasses[appClassId]);
if (appClass.parentAppClassId == 0) {
// Flip the appClass Show Property
appClasses[appClass.appClassId].show = appClasses[appClass.appClassId].show ? false : true;
if (Object.keys(appClasses[appClass.appClassId].children).length !== 0) {
// Regardless if we enable or disabled the parent, all children should be disabled
for (var childKey in appClasses[appClass.appClassId].children) {
appClasses[appClass.appClassId].children[childKey].show = false;
}
}
} else {
// If we are enabling a child, make sure parent is disabled
if (!appClasses[appClass.parentAppClassId].children[appClass.appClassId].show) {
appClasses[appClass.parentAppClassId].show = false;
}
appClasses[appClass.parentAppClassId].children[appClass.appClassId].show = appClasses[appClass.parentAppClassId].children[appClass.appClassId].show ? false : true;
}
dispatch(handleUpdateInitialSourceFetch(appClasses));
return { type: types.TOGGLE_APP_CLASS, appClasses };
}
I already have appClasses in the state, what I wanted was to send the modified appClasses and update the state in the reducer. I want to to it this way because I read that reducers should be simple, and the modification as you can see in the action are complex. Can I do this kind of complex modifications in the reducer? Because here is giving me an error because I'm sending this.props.appClasses. If I do this on the reducer I don't have to send appClasses because is on the state of the reducer.
So in one line, can I make this complex modifications on a reducer instead of make them on the action?
You can create a function that computes the new appClasses state and dispatch two actions sending this new state inside each action.
Then you dispatch this function itself using redux-thunk middleware.
It will look like this:
function handleSomeUserInteraction(appClasses, parentAppClassId, appClassId) {
return function(dispatch) {
//make a copy of appClasses, so you wont change the state directly
let newAppClasses = Object.assign({}, appClasses)
//do all logic work to compute new appClasses
//...
//...
//handleUpdateInitialSourceFetch
dispatch({ type: types.UPDATE_INITIAL_SOURCE_FETCH, appClasses: newAppClasses }
//handleToggleAppClass
dispatch({ type: types.TOGGLE_APP_CLASS, appClasses: newAppClasses })
}
}
You can learn more about this here:
http://jamesknelson.com/can-i-dispatch-multiple-actions-from-redux-action-creators/

The proper place to change state in react redux

As the doc says:
Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().
If I follow the principle, there are some questions about the code orgnization (my app is a file manager).
For example,
default reducer like this:
export default function (state = initialState, action) {
const { path } = action
if (typeof path === 'undefined') {
return state
}
const ret = {
...state,
[path]: parentNode(state[path], action)
};
switch (action.type) {
case OPEN_NODE:
case GO_PATH:
ret['currentPath'] = path
break
default:
break
}
return ret
}
data struct in state[path] likes:
{
'open': false,
'path': '/tmp/some_folder',
'childNodes' : [ {'path':'/some/path', 'mode': '0755', 'isfolder': true}, ....],
'updateTime': Date.now()
}
Now I need several actions such as ADD_CHILD, DELETE_CHILD , RENAME_CHILD, MOVE_CHILD, there are two sulotions(by change state in actions or reducers):
1. All functional code in actions:
actions:
export function updateChildNodes(path, nodes) {
return {
type: UPDATE_CHILD_NODES,
path: path,
loading: false,
loaded: true,
childNodes: nodes,
};
}
export function addChild(path, node) {
return (dispatch, getState) => {
const state = getState().tree[path]
var childNodes = state.childNodes ? state.childNodes :[]
childNodes.push(node)
return dispatch(updateChildNodes(path, childNodes))
}
}
export function deleteChild(parent_path, child_node) {
return (dispatch, getState) => {
const state = getState().tree[parent_path]
var childNodes = state && state.childNodes ? state.childNodes : []
for (var i=0; i <=childNodes.length; i++){
if (childNodes[i].path == child_node.path){
childNodes.splice(i, 1)
return dispatch(updateChildNodes(parent_path, childNodes))
}
}
}
}
export function deleteNode(node) {
return (dispatch, getState) => {
// ajax call
return api.deleteChild(node.path, () => {
dispatch(deleteChild(node.parent, node))
})
}
}
.....
parentNode reducer:
function parentNode(state, action) {
switch (action.type) {
case UPDATE_CHILD_NODES:
return {
...state,
childNodes: action.childNodes
}
default:
return state;
}
}
All variable pass in parentNode from actions, parentNode just assign change to state doesn't do anything else.
All logic of remove node and add node is done by actions, only UPDATE_CHILD_NODES in parentNode.
2. Action just send data to reducer, let reducer to process
actions:
export function updateChildNodes(path, nodes) {
return {
type: UPDATE_CHILD_NODES,
path: path,
loading: false,
loaded: true,
childNodes: nodes,
};
}
export function addChild(path, node) {
return {
type: ADD_CHILD,
path: path,
node: node,
};
}
export function deleteChild(path, node) {
return {
type: DELETE_CHILD,
path: path,
node: node,
};
}
export function deleteNode(node) {
return (dispatch, getState) => {
// ajax call
return api.deleteChild(node.path, () => {
dispatch(deleteChild(node.parent, node))
})
}
}
.....
parentNode reducer:
function parentNode(state, action) {
switch (action.type) {
case DELETE_CHILD:
let childNodes = state.childNodes.slice() // have to clone obj
for (var i=0; i <=childNodes.length; i++){
if (childNodes[i].path == action.node.path){
childNodes.splice(i, 1)
}
}
return {
...state,
childNodes: childNodes
};
case ADD_CHILD:
let childNodes = state.childNodes.slice() // have to clone obj
childNodes.push(node)
return {
...state,
childNodes: childNodes
};
case UPDATE_CHILD_NODES:
return {
...state,
childNodes: action.childNodes
}
default:
return state;
}
}
In my option, the solution 2 is more readable and pretty.
But is it good to change the state by mutate an cloned obj? And when I need set updateTime by Date.now(), I have to generate it from actions and pass to reducer,so that state variables are generated in different place(But I'd like put them together...)
Any opinion for this?
From this redux discussion here:
It is best practice to place most of the logic in the action creators and leave the reducers as simple as possible (closer to your option 1)
for the following reasons:
Business logic belongs in action-creators. Reducers should be stupid and simple. In many individual cases it does not matter- but consistency is good and so it's best to consistently do this. There are a couple of reasons why:
Action-creators can be asynchronous through the use of middleware like redux-thunk. Since your application will often require asynchronous updates to your store- some "business logic" will end up in your actions.
Action-creators (more accurately the thunks they return) can use shared selectors because they have access to the complete state. Reducers cannot because they only have access to their node.
Using redux-thunk, a single action-creator can dispatch multiple actions- which makes complicated state updates simpler and encourages better code reuse.
For small apps I usually put my logic in action creators. For more complex situations you may need to consider other options. Here is a summary on pros and cons of different approaches: https://medium.com/#jeffbski/where-do-i-put-my-business-logic-in-a-react-redux-application-9253ef91ce1#.k8zh31ng5
Also, have a look at Redux middleware.
The middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
This is an answer provided by Dan Abramov (author of Redux): Why do we need middleware for async flow in Redux?
And here are the official Redux docs: http://redux.js.org/docs/advanced/Middleware.html

Sharing data between two Redux Reducers/States

Is this a reasonable solution for data sharing between two states/reducers?
//combineReducers
function coreReducer(state = {}, action){
let filtersState = filters(state.filters, action);
let eventsState = events(state.events, action, { filters: filtersState});
return { events: eventsState, filters : filtersState};
}
export const rootReducer = combineReducers(
{
core : coreReducer,
users
}
);
If so, how can one guarantee the order in which reducer functions are executed if both answer to the same dispatched event and the second reducing function depends on the new state of the first one?
Let's say that we dispatch a SET_FILTER event that appends to activeFilters collection in the filters Store and later changes the visibility of items in the events Store with respect to the activeFilters values.
//ActiveFilters reducer
function filtersActions(state = {}, action){
switch (action.type) {
case SET_FILTER:
return Object.assign({}, state, {
[action.filterType]: action.filter
})
case REMOVE_FILTER:
var temp = Object.assign({}, state);
delete temp[action.filterType];
return temp;
case REMOVE_ALL_FILTERS:
return {};
default:
return state
}
}
I think I found the answer - Computing Derived Data - Reselect
http://redux.js.org/docs/recipes/ComputingDerivedData.html
/--------container--------/
import {getGroupsAndMembers} from '../reducers'
const mapStateToProps = (state) => {
return {
inputValue: state.router.location.pathname.substring(1),
initialState: getGroupsAndMembers(state) <-- this one
}
}
/--------reducers--------/
export function getGroupsAndMembers(state){
let { groups, members } = JSON.parse(state)
response = {groups, members}
return response;
}
GroupsContainer.propTypes = {
//React Redux injection
pushState: PropTypes.func.isRequired,
// Injected by React Router
children: PropTypes.node,
initialState:PropTypes.object,
}
don't forget to follow the guidelines for 'connect'
export default connect(mapStateToProps,{ pushState })(GroupsContainer)
If you have two reducers, and one depend on a value from a first one, you just have to update them carefully, and the best solution will be just to use a special function, which will first set the filtering, and then query corresponding events. Also, keep in mind that if events fetching is asynchronous operation, you should also nest based on filtering type -- otherwise there is a chance of race condition, and you will have wrong events.
I have created a library redux-tiles to deal with verbosity of raw redux, so I will use it in this example:
import { createSyncTile, createTile } from 'redux-tiles';
const filtering = createSyncTile({
type: ['ui', 'filtering'],
fn: ({ params }) => params.type,
});
const events = createTile({
type: ['api', 'events'],
fn: ({ api, params }) => api.get('/events', { type: params.type }),
nesting: ({ type }) => [type],
});
// this function will just fetch events, but we will connect to apiEvents
// and filter by type
const fetchEvents = createTile({
type: ['api', 'fetchEvents'],
fn: ({ selectors, getState, dispatch, actions }) => {
const type = selectors.ui.filtering(getState());
return dispatch(actions.api.events({ type }));
},
});

Categories