Running a React functional component with redux store.subscribe - reactjs

Consider the below code made in react ( also including redux)
store = createStore(todoApp) ;
store.subscribe(App);
export default function App(){
.....
}
which means for every dispatch action happening in my App functional component the App must render since subscribe executes the enclosed function . However what I have observed is the that though the function executes the HTML components do not get updated and remain the same from the first render even after multiple dispatch actions . Can anyone explain this behavior ?

That is not how you connect your React application to the redux store. Here is an example application using Provider and the react-redux hooks:
//you would import these with
// import {Provider} from 'react-redux'
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const { memo, useMemo, useCallback } = React;
const initialState = {
counters: [
{ id: 1, count: 1 },
{ id: 2, count: 1 },
{ id: 3, count: 1 },
],
};
//action types
const ADD = 'ADD';
//action creators
const add = (id) => ({
type: ADD,
payload: id,
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
return {
...state, //not needed in this case but usually is
counters: state.counters.map(
(counter) =>
counter.id === payload
? { ...counter, count: counter.count + 1 }
: counter //do not update this counter (not the right id)
),
};
}
return state;
};
//selectors
const selectCounters = (state) => state.counters;
const createSelectCounterById = (counterId) =>
createSelector(
[selectCounters], //re use select counters
(
counters //got the counters, find the right counter
) => counters.find(({ id }) => id === counterId)
);
//select sum of all counters
const selectSum = createSelector(
[selectCounters], //re use select counters
(counters) =>
//reduce counters array to a number
counters.reduce(
(result, counter) => result + counter.count,
0
)
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const Counter = memo(function Counter({ id, addAction }) {
const selectCounter = useMemo(
() => createSelectCounterById(id),
[id]
);
const counter = useSelector(selectCounter);
return (
<li>
{counter.count}
<button onClick={() => addAction(id)}>+</button>
</li>
);
});
const Total = memo(function Total() {
const sum = useSelector(selectSum);
return <h3>{sum}</h3>;
});
const App = () => {
const counters = useSelector(selectCounters);
const dispatch = useDispatch();
const addAction = useCallback(
(id) => dispatch(add(id)),
//dispatch is not really a dependency but
// linter doesn't know that and adding
// it doesn't change behavior
[dispatch]
);
return (
<div>
<Total />
<ul>
{counters.map(({ id }) => (
//id property here is not html id element property
<Counter key={id} id={id} addAction={addAction} />
))}
</ul>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>

Related

React.MEMO() fails to work with hooks inside of it (probably useDispatch) within KendoReact

I have a child filter input componenet inside kendo grid and my goa is stoping that components from rendering again and saving "input text" inside input field
<GridColumn
field="name"
title="Document Name"
headerCell={HeaderCell}
className="tableCell"
cell={LinkCell}
filterCell={() => <SearchInput className={filtersToggler} /> }
footerCell={(props) => <FooterCell {...props} colSpan={4} total={total}></FooterCell>}
/>
Now when I add some input inside that component, it passes value to the state called word and after 500 ms it triggers debounce and parses it to the redux state called "term"
const SearchInput = React.memo(() => {
const [word, setWord] = useState('');
const dispatch = useDispatch();
const deb = useCallback(
debounce((text) => dispatch({ type: 'SET_TERM', term: text }), 1000),
[]
);
const handleText = (text) => {
deb(text);
};
return (
<input
className="searchInput"
value={word}
type="search"
placeholder="Search.."
onChange={(e) => {
handleText(e.target.value)
setWord(e.target.value);
}}></input>
);
});
export default SearchInput;
Now, whenever redux state changes, it triggers useEffect inside of a kendo grid and gets new data from API.
const searchWord = useSelector((state) => state.search.term);
const classifications = useSelector((state) => state.search.classifications);
const date = useSelector((state) => state.search.date);
useEffect(() => {
const data = searchDocsByName(searchWord, date, classifications);
data.then((i) => {
setDocuments(i.data.data);
setTotal(i.data.data.length);
});
}, [searchWord, date, classifications]);
So what's the problem? SearchInput Componenet rerenders even if its inside React.memo() and from the profiler I am getting that SearchInput rendered because "hook change".
I am totally stuck, I have no idea how to procced.
You needlessly set local state with const [word, setWord] = useState('');, setWord will re render your component because local state changed. You can make the input an uncontrolled component
Here is an example of what you can do:
const { Provider, useDispatch } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { useCallback } = React;
const initialState = {};
const reducer = (state, { type, term }) => {
console.log('in reducer', type, term);
return state;
};
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
() => (next) => (action) => next(action)
)
)
);
const debounce = (fn, time) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), time);
};
};
const SearchInput = React.memo(function SearchInput() {
console.log('rendering SearchInput');
const dispatch = useDispatch();
const deb = useCallback(
debounce(
(text) => dispatch({ type: 'SET_TERM', term: text }),
1000
),
[]
);
return (
<input
className="searchInput"
type="search"
placeholder="Search.."
onChange={(e) => {
deb(e.target.value);
}}
></input>
);
});
const App = () => {
return <SearchInput />;
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>

React-Redux Functional Component Multiple Renders

I created a very simple React-Redux App and fetching Users and Posts from https://jsonplaceholder.typicode.com/
In my components I am logging Users and Posts data into the console. As far as I see, in the network tab there is one request for Users and 10 requests for Posts. That's correct but in the console, I see 10 Posts requests for each User.
Does it mean ReactJS renders the component 100 times? What is my mistake in this code?
Any help will be greatly appreciated!
My code and codepen link are below
Please check the code in codepen
const { useEffect } = React;
const { connect, Provider } = ReactRedux;
const { createStore, applyMiddleware, combineReducers } = Redux;
const thunk = ReduxThunk.default;
//-- REDUCERS START -- //
const userReducer = (state = [], action) => {
if (action.type === 'fetch_users') return [...action.payload];
return state;
};
const postReducer = (state = [], action) => {
if (action.type === 'fetch_posts') return [...action.payload];
return state;
};
//-- REDUCERS END -- //
//-- ACTIONS START -- //
const fetchUsers = () => async dispatch => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
dispatch({ type: 'fetch_users', payload: response.data });
};
const fetchPosts = userId => async dispatch => {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${userId}/posts`
);
dispatch({ type: 'fetch_posts', payload: response.data });
};
//-- ACTIONS END -- //
const reducer = combineReducers({ users: userReducer, posts: postReducer });
const store = createStore(reducer, applyMiddleware(thunk));
const mapStateToProps = state => {
return { users: state.users, posts: state.posts };
};
const mapDispatchToProps = dispatch => {
return {
getUsers: () => dispatch(fetchUsers()),
getPosts: (id) => dispatch(fetchPosts(id))
};
};
const Users = props => {
console.log('users', props.users);
const { getUsers } = props;
useEffect(() => {
getUsers();
}, [getUsers]);
const renderUsers = () =>
props.users.map(user => {
return (
<div>
<div>{user.name}</div>
<div>
<PostsContainer userId={user.id} />
</div>
</div>
);
});
return <div style={{backgroundColor:'green'}}>{renderUsers()}</div>;
};
const UserContainer = connect(mapStateToProps, mapDispatchToProps)(Users);
const Posts = props => {
console.log('posts' , props.posts);
const { getPosts, userId } = props;
useEffect(() => {
getPosts(userId);
}, [getPosts, userId]);
const renderPosts = () =>
props.posts.map(post => {
return (
<div>
<div>{post.title}</div>
</div>
);
});
return <div style={{backgroundColor:'yellow'}}>{renderPosts()}</div>;
};
const PostsContainer = connect(mapStateToProps, mapDispatchToProps)(Posts);
const App = props => {
return (
<div>
<UserContainer />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Does it mean ReactJS renders the component 100 times? What is my mistake in this code?
you have a UserContainer, that renders and requests for users;
once fetched users, you have an update state. UserContainer rerenders, and now you have 10 PostContainers;
each PostContainer makes a request to fetch posts, 10 on total;
it results in 10 state updates. UserContainer rerenders 10 times, and each PostContainer rerenders 10 times;
The component doesn't renders 100 times, each PostContainer renders the initial mount then rerenders 10 times. since there are 10 PostContainers and each rerenders 10 times that's why you might think that renders 100 times.
you have some issues. the dependency issue, which was pointed out is the first. getUsers useEffect should have an empty dependency, and userId useEffect, should depend on userId.
to solve the 10 rerenders on UserContainer due to posts, you need to have a different mapStateToProps to each. for UserContainer you will map only users, otherwise you will get 10 updates due to 10 posts requests:
const mapUserStateToProps = state => {
return { users: state.users };
};
with that it solves UserContainer 10 rerenders.
now about PostContainer there is something that needs to be fixed first, your reducer. your reducer replaces last posts with the current call. in the end you will have only the posts that arrived last, not all posts. to fix that you need to spread your state.
const postReducer = (state = [], action) => {
if (action.type === 'fetch_posts') return [...state, ...action.payload];
return state;
};
eventually, if in your project you could have a repeated request to same userId than it would be necessary to have some validation for not adding the same posts again
now it leads us to mapping props to PostContainer. you would need to have a filter on posts based on userId. mapStateToProps takes props as second argument, which enables us to accomplish that:
const mapPostStateToProps = (state, { userId }) => {
return { posts: state.posts.filter(post => post.userId === userId) };
};
this looks the end to solve the issue, but each PostContainer still rerenders 10 times. why does this happens since posts will be the same? that happens because filter will return a new array reference, no matter if its content didn't change.
to solve this issue you can use React.memo. you need to provide the component and a equality function to memo. to compare an array of objects there are some solutions, also few libs that provide some deepEqual function. here I use JSON.stringify to compare, but you are free to use some other one:
const areEqual = (prevProps, nextProps) => {
return JSON.stringify(prevProps.posts) === JSON.stringify(nextProps.posts)
}
you would validate also other props that could change but that's not the case
now apply React.memo to posts:
const PostsContainer = connect(mapPostStateToProps, mapDispatchToProps)(React.memo(Posts, areEqual));
After all that applied, UserContainer will rerender one once, and each PostContainer will rerender only once as well.
here follows link with working solution:
https://codepen.io/rbuzatto/pen/BaLYmNK?editors=0010
final code:
const { useEffect } = React;
const { connect, Provider } = ReactRedux;
const { createStore, applyMiddleware, combineReducers } = Redux;
const thunk = ReduxThunk.default;
//-- REDUCERS START -- //
const userReducer = (state = [], action) => {
if (action.type === 'fetch_users') return [...action.payload];
return state;
};
const postReducer = (state = [], action) => {
if (action.type === 'fetch_posts') return [...state, ...action.payload];
return state;
};
//-- REDUCERS END -- //
//-- ACTIONS START -- //
const fetchUsers = () => async dispatch => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
dispatch({ type: 'fetch_users', payload: response.data });
};
const fetchPosts = userId => async dispatch => {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${userId}/posts`
);
dispatch({ type: 'fetch_posts', payload: response.data });
};
//-- ACTIONS END -- //
const reducer = combineReducers({ users: userReducer, posts: postReducer });
const store = createStore(reducer, applyMiddleware(thunk));
const mapUserStateToProps = state => {
return { users: state.users };
};
const mapPostStateToProps = (state, { userId }) => {
return { posts: state.posts.filter(post => post.userId === userId) };
};
const mapDispatchToProps = dispatch => {
return {
getUsers: () => dispatch(fetchUsers()),
getPosts: (id) => dispatch(fetchPosts(id))
};
};
const Users = props => {
console.log('users', props.users);
const { getUsers } = props;
useEffect(() => {
getUsers();
}, []);
const renderUsers = () =>
props.users.map(user => {
return (
<div key={user.id}>
<div>{user.name}</div>
<div>
<PostsContainer userId={user.id} />
</div>
</div>
);
});
return <div style={{backgroundColor:'green'}}>{renderUsers()}</div>;
};
const UserContainer = connect(mapUserStateToProps, mapDispatchToProps)(Users);
const Posts = props => {
console.log('posts');
const { getPosts, userId } = props;
useEffect(() => {
getPosts(userId);
}, [userId]);
const renderPosts = () =>
props.posts.map(post => {
return (
<div>
<div>{post.title}</div>
</div>
);
});
return <div style={{backgroundColor:'yellow'}}>{renderPosts()}</div>;
};
const areEqual = (prevProps, nextProps) => {
return JSON.stringify(prevProps.posts) === JSON.stringify(nextProps.posts)
}
const PostsContainer = connect(mapPostStateToProps, mapDispatchToProps)(React.memo(Posts, areEqual));
const App = props => {
return (
<div>
<UserContainer />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
useEffect() renders the component every time something is changed in the dependencies you provided.
Ideally, you should change your components to re-render only when something changes in props. getUser and getPost change on each render. So, it is better to change it to monitor users and posts from state.
In Users:
const { users, getUsers } = props;
useEffect(() => {
getUsers();
}, []); -- Leaving this empty makes it load only on mount.
In Posts:
const { getPosts, userId } = props;
useEffect(() => {
getPosts(userId);
}, [userId]);

Redux in React to call multiple dispatch functions

I have an index.js which calls an action. Once it calls an action I want to trigger multiple dispatch actions inside that action. Index.js calls handLoadCustomers which will dispatch loadCustomers function which calls an API and dispatches another function to store customers in the state.
Once that is done, call comes back to handleLoadCustomers where I want to use customers from the first
call and then dispatch another action of handleExtraCustomerLoads with those customers which will
call another functions/actions. How can I do that in React Redux?
export function handleLoadCustomers() {
return function (dispatch) {
dispatch(loadCustomers())
.then((customers) => {
dispatch(handleExtraCustomerLoads(customers));
})
.catch((error) => {
throw error;
})
.then((newCustomers) => {
dispatch(handlePageLoadSuccess(newCustomers));
});
};
export function loadCustomers() {
return function (dispatch) {
return getCustomers()
.then((customers) => {
dispatch(loadCustomerSuccess(customers));
})
.catch((error) => {
throw error;
});
};
}
customers after the loadCustomers is empty and it does not dispatch handleExtraCustomerLoads function at all
You should not use the return value of your action but you can wait for it to finish and select the result from the state (I assume the action will write data you need to the store).
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = {
customers: [],
extra: [],
};
//api
const getCustomers = () => Promise.resolve([1, 2, 3]);
//action types
const CUSTOMERS_SUCCESS = 'CUSTOMERS_SUCCESS';
const EXTRA = 'EXTRA';
//action creators
const loadCustomerSuccess = (customers) => ({
type: CUSTOMERS_SUCCESS,
payload: customers,
});
const handleExtraCustomerLoads = (extra) => ({
type: EXTRA,
payload: extra,
});
function handleLoadCustomers() {
return function (dispatch, getState) {
dispatch(loadCustomers()).then(() => {
const customers = selectCustomers(getState());
dispatch(handleExtraCustomerLoads(customers));
});
};
}
function loadCustomers() {
return function (dispatch) {
return getCustomers()
.then((customers) => {
//you need to return the value here
return dispatch(loadCustomerSuccess(customers));
})
.catch((error) => {
throw error;
});
};
}
const reducer = (state, { type, payload }) => {
if (type === CUSTOMERS_SUCCESS) {
return { ...state, customers: payload };
}
if (type === EXTRA) {
return { ...state, extra: payload };
}
return state;
};
//selectors
const selectCustomers = (state) => state.customers;
const selectExtra = (state) => state.extra;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware((store) => (next) => (action) =>
//diy thunk
typeof action === 'function'
? action(store.dispatch, store.getState)
: next(action)
)
)
);
const App = () => {
const dispatch = useDispatch();
const customers = useSelector(selectCustomers);
const extra = useSelector(selectExtra);
return (
<div>
<button
onClick={() => dispatch(handleLoadCustomers())}
>
load customers
</button>
<pre>
{JSON.stringify({ customers, extra }, undefined, 2)}
</pre>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>

How to update local state when redux state is updated(react-redux)

I have state that is managed inside a component.
And I'm using data from other component by storing them into the redux store.
However, at the end, i'm trying to collect all the data to make an api call.
const [projectData, setProjectData] = useState({})
const { userData } = useSelector((state) => state.userManage);
useEffect(() => {
let newUsers = userData.map((x,i)=>({user_id:x.user_id, sub_user_id:x.sub_user_id}))
setProjectState((state) =>({...state, users:newUsers}))
}, [userData ]);
// this is handler
const handleClick = () =>{
dispatch(action.createProject(projectData));
}
I'm trying to update local state whenever the data i'm subscribing is changing.
Somehow it doesn't update correctly. When I don't use 'map' function and just
setProjectState((state) =>({...state, users: userData})) <= This works.
But it copys everything inside of userData,
so I'm using map function to pick specific data, not all data. but it is not working.
for example:
//redux store state
userData={user_id: 0, sub_id: [1,2,3], useless:"sda", useless2:"asd" }
I want to update and add local state only with user_id and sub_id
The code you posted works just fine, maybe you can provide an example that demonstrates your problem assuming you meant setProjectData instead of setProjectState:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { useState, useEffect } = React;
const initialState = {
userData: [],
};
//action types
const ADD = 'ADD';
//action creators
const userid = ((id) => () => id++)(1);
const sub = ((id) => () => id++)(1);
const add = () => ({
type: ADD,
payload: {
user_id: userid(),
sub_user_id: sub(),
other: 'not used',
},
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
return {
...state,
userData: [payload, ...state.userData],
};
}
return state;
};
//selectors
const selectUserData = (state) => state.userData;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const UserList = () => {
const [projectData, setProjectData] = useState({});
const userData = useSelector(selectUserData);
useEffect(() => {
let newUsers = userData.map((x, i) => ({
user_id: x.user_id,
sub_user_id: x.sub_user_id,
}));
setProjectData((state) => ({
...state,
users: newUsers,
}));
}, [userData]);
return (
<div>
userData:
<pre>{JSON.stringify(userData, undefined, 2)}</pre>
projectData:
<pre>{JSON.stringify(projectData, undefined, 2)}</pre>
</div>
);
};
const App = () => {
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(add())}>
add data
</button>
<UserList />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
However; instead of copying the data it would be best to just write a selector to get the data the way you want it:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
userData: [],
};
//action types
const ADD = 'ADD';
//action creators
const userid = ((id) => () => id++)(1);
const sub = ((id) => () => id++)(1);
const add = () => ({
type: ADD,
payload: {
user_id: userid(),
sub_user_id: sub(),
other: 'not used',
},
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
return {
...state,
userData: [payload, ...state.userData],
};
}
return state;
};
//selectors
const selectUserData = (state) => state.userData;
const selectProjectData = createSelector(
[selectUserData], //re use selectUserData
(userData) =>
//modify user data to project data
userData.map((x, i) => ({
user_id: x.user_id,
sub_user_id: x.sub_user_id,
}))
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const UserList = () => {
//no state, no effect, just select the way you want it
const projectData = useSelector(selectProjectData);
//if you want userData as well you can select that too
// const userData = useSelector(selectUserData)
return (
<div>
<pre>{JSON.stringify(projectData, undefined, 2)}</pre>
</div>
);
};
const App = () => {
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(add())}>
add data
</button>
<UserList />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>

How immer push in an array triggers an update in redux

I noticed that immer doesn't create a new array when doing draft.list.push()
Why this is a suggested approach when this doesn't work for redux? Is spread operator the only solution to this? The actual gain from this is that we are sure that array objects will not be mutated?
I am a little confused of the actual purpose of this action
Without seeing any code it is impossible to say what's wrong but there is nothing wrong with immer using push:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const { produce } = immer;
const id = ((id) => () => id++)(4);
const initialState = {
data: {
counters: [
{ id: 1, count: 0 },
{ id: 2, count: 0 },
{ id: 3, count: 0 },
],
},
};
//action types
const INCREASE = 'INCREASE';
const ADD = 'ADD';
//action creators
const increase = (id) => ({
type: INCREASE,
payload: id,
});
const add = () => ({
type: ADD,
});
const reducer = (
state = initialState,
{ type, payload }
) => {
if (type === INCREASE) {
const index = state.data.counters.findIndex(
({ id }) => id === payload
);
//you sure you return here?
return produce(state, (draft) => {
++draft.data.counters[index].count;
});
}
if (type === ADD) {
//returning new state
return produce(state, (draft) => {
//using arry push on immer draft
draft.data.counters.push({ id: id(), count: 0 });
});
}
return state;
};
//selectors
const selectCounters = (state) => state.data.counters;
const createSelectCounterById = (counterId) =>
createSelector([selectCounters], (counters) =>
counters.find(({ id }) => id === counterId)
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const Counter = React.memo(function Counter({ id }) {
const dispatch = useDispatch();
const selectCounter = React.useMemo(
() => createSelectCounterById(id),
[id]
);
const counter = useSelector(selectCounter);
console.log('rendering:', id);
return (
<li>
{id}:{' '}
<button onClick={() => dispatch(increase(id))}>
{counter.count}
</button>
</li>
);
});
const App = () => {
const counters = useSelector(selectCounters);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(add())}>
add counter
</button>
<ul>
{counters.map((counter) => (
<Counter key={counter.id} id={counter.id} />
))}
</ul>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://unpkg.com/immer#7.0.5/dist/immer.umd.production.min.js"></script>
<div id="root"></div>

Resources