React-redux does not re-render on state change - javascript

I just started with redux and react-redux. I am observing a very weird behavior and not able to wrap my head around it.
I am trying something like this.
const fetchedFolders = useSelector(state=>{
console.log("redux state = ",state);
return state.fetchedFolders;
});
const updateFetchedFolders = useDispatch();
I have callback function that receives a new set of values and will update the state in store.
let appendFoldersToList=(newFolders)=>{
console.log(typeof(newFolders))
if(typeof(newFolders) === undefined)
console.log("go to error");
else{
updateFetchedFolders(setFetchedFolders([...fetchedFolders,...newFolders]));
}
}
this works perfectly and re-renders the list with new value
but if I replace the line
updateFetchedFolders(setFetchedFolders([...fetchedFolders,...newFolders]));
with
updateFetchedFolders(setFetchedFolders([...newFolders]));
it does not re-render and it still shows the old list. but in console, I can see data is updated.
I am not able to understand why it re-renders in first case and not in second case.
This is how my reducers look:-
export const reducer = (state=initialState, action)=>{
switch(action.type){
case 'SET_FOLDERS': return {
...state,
fetchedFolders:[...action.payload]
}
}
}
this is my action creator:-
export const setFetchedFolders = (payload)=>{
return {
type:'SET_FOLDERS',
payload:payload
}
}
this is my initial state:-
const initialState = {
fetchedFolders:[],
}
I don't think I am not mutating the state.
my array looks something like this::-
[
{name:cats, id:SOME_ID},
{name:dogs, id:SOME_ID}
]

Related

React hook, wired issue when use useState, while if use setState work perfectly, how to solve it

dear community, I am facing a wired issue, and I don't know how to summary my situation in the question title, so I wonder if the question title is accurate enough.
I was trying to convert a class component to a hook component.
The class version code like this
async componentDidMount() {
const { dispatch, itemId } = this.props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment', //here to fetch submissions in props
payload: {
id: itemId
}
});
}
const { submissions } = this.props;
this.setState({
studentSubmissions: submissions,
});
} catch (error) {
throw error.message;
}
}
render() {
const { studentSubmissions } = this.state;
return (
<Table dataSource={studentSubmissions} />
)
}
export default SubmissionsDetail;
and in hook, it look like this
const [studentSubmissions, setStudentSubmissions] = useState([]);
useEffect(() => {
async function fetchSubmissions() {
const { dispatch, itemId } = props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment',
payload: {
id: itemId
}
});
}
const { submissions } = props;
setStudentSubmissions(submissions)
} catch (error) {
throw error.message;
}
};
fetchSubmissions()
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
I omitted some code for better reading, like connect to redux store or others.
and the component is import in the parent file like this
import SubmissionsDetail from './SubmissionsDetail'
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId}/>
})}
it work perfect in class component, the expected result should return tables like this
However, when I change to use hook, the result return like this
or sometimes all data in tables become submissions3
I try to console.log(submissions) inside the try{...} block, when in class, the result is
which is correct, there have two assignments, the one have 4 submissions, another one have zero submission.
But the output in hook is different, the result is like this
either both have 4 submissions, either both have zero. That means one obj affect all other obj.
It seems like if useState change, it would influence other objs, that make me really confused. I think in the map method, each item is independent, right? If so, and how to explain why it work perfectly in class setState, but failed in hook useState?
I hope my question is clear enough, If you know how to describe my question in short, plz let me know, I would update the title, to help locate experts to answer.
Please don't hesitate to share your opinions, I really appreciate and need your help, many thanks!
Edit: You are probably going to want to rework the way you store the submission inside of the redux store if you really want to use the Hook Component. It seems like right now, submissions is just an array that gets overwritten whenever a new API call is made, and for some reason, the Class Component doesn't update (and it's suppose to update).
Sorry it's hard to make suggestions, your setup looks very different than the Redux environments I used. But here's how I would store the submissions:
// no submissions loaded
submissions: {}
// loading new submission into a state
state: {
...state,
sessions: {
...state.session,
[itemId]: data
}
}
// Setting the state inside the component
setStudentSubmissions(props.submissions[itemId])
And I think you will want to change
yield put({
type: 'getSubmissions',
payload: response.data.collections
});
to something like
yield put({
type: 'getSubmissions',
payload: {
data: response.data.collections,
itemId: id
});
If you want to try a "hack" you can maybe get a useMemo to avoid updating? But again, you're doing something React is not suppose to do and this might not work:
// remove the useEffect and useState, and import useMemo
const studentSubmissions = useMemo(async () => {
try {
if (itemId) {
await dispatch({
type: "assignment/fetchSubmissionsByAssignment", //here to fetch submissions in props
payload: {
id: itemId,
},
});
return this.props.submissions;
}
return this.props.submissions;
} catch (error) {
throw error.message;
}
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
There is no reason to use a local component state in either the class or the function component versions. All that the local state is doing is copying the value of this.props.submissions which came from Redux. There's a whole section in the React docs about why copying props to state is bad. To summarize, it's bad because you get stale, outdated values.
Ironically, those stale values were allowing it to "work" before by covering up problems in your reducer. Your reducer is resetting the value of state.submissions every time you change the itemId, but your components are holding on to an old value (which I suspect is actually the value for the previous component? componentDidMount will not reflect a change in props).
You want your components to select a current value from Redux based on their itemId, so your reducer needs to store the submissions for every itemId separately. #Michael Hoobler's answer is correct in how to do this.
There's no problem if you want to keep using redux-saga and keep using connect but I wanted to give you a complete code so I am doing it my way which is with redux-toolkit, thunks, and react-redux hooks. The component code becomes very simple.
Component:
import React, { useEffect } from "react";
import { fetchSubmissionsByAssignment } from "../store/slice";
import { useSelector, useDispatch } from "../store";
const SubmissionsDetail = ({ itemId }) => {
const dispatch = useDispatch();
const submissions = useSelector(
(state) => state.assignment.submissionsByItem[itemId]
);
useEffect(() => {
dispatch(fetchSubmissionsByAssignment(itemId));
}, [dispatch, itemId]);
return submissions === undefined ? (
<div>Loading</div>
) : (
<div>
<div>Assignment {itemId}</div>
<div>Submissions {submissions.length}</div>
</div>
);
};
export default SubmissionsDetail;
Actions / Reducer:
import { createAsyncThunk, createReducer } from "#reduxjs/toolkit";
export const fetchSubmissionsByAssignment = createAsyncThunk(
"assignment/fetchSubmissionsByAssignment",
async (id) => {
const response = await getSubmissionsByAssignment(id);
// can you handle this in getSubmissionsByAssignment instead?
if (response.status !== 200) {
throw new Error("invalid response");
}
return {
itemId: id,
submissions: response.data.collections
};
}
);
const initialState = {
submissionsByItem: {}
};
export default createReducer(initialState, (builder) =>
builder.addCase(fetchSubmissionsByAssignment.fulfilled, (state, action) => {
const { itemId, submissions } = action.payload;
state.submissionsByItem[itemId] = submissions;
})
// could also respond to pending and rejected actions
);
if you have an object as state, and want to merge a key to the previous state - do it like this
const [myState, setMyState] = useState({key1: 'a', key2: 'b'});
setMyState(prev => {...prev, key2: 'c'});
the setter of the state hook accepts a callback that must return new state, and this callback recieves the previous state as a parameter.
Since you did not include large part of the codes, and I assume everything works in class component (including your actions and reducers). I'm just making a guess that it may be due to the omission of key.
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId} key={itemId} />
})}
OR it can be due to the other parts of our codes which were omitted.

Redux | Why this store's parameter type changes after second click?

I'm trying to push a new value in the store's state. It works fine the first time I click on the button "Add item", but the second time I got the following error: "state.basket.push is not a function". I configure the action to console log the state and got the following results:
1st click: {...}{basketItems: Array [ "44" ]}
2nd click: Object {basketItems: 0 }
Why the variable type is changing from array to an int?
Here is the code for the rendered component:
function Counter({ basketItems,additem }) {
return (
<div>
<button onClick={additem}>Add item</button>
</div>
);
}
const mapStateToProps = state => ({
basketItems: state.counterReducer.basketItems,
});
const mapDispatchToProps = dispatch => {
return {
additem: ()=>dispatch({type: actionType.ADDITEM, itemName:'Dummy text' }),
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
And the reducer looks like this:
import {ADDITEM} from "../actions/types";
const initialState = { basket: [], };
export default function reducer(state = initialState, action) {
switch (action.type) {
case ADDITEM:
console.log(state);
// let newBasket = state.basket.push('44');
return {
...state,
basket: state.basket.push('44')
};
default:
return state;
}
}
I'm copying the state before updating the basket to prevent weird behaviors.
There's two problems here:
state.basket.push() mutates the existing state.basket array, which is not allowed in Redux
It also returns the new size of the array, not an actual array
So, you're not doing a correct immutable update, and you're returning a value that is not an array.
A correct immutable update here would look like:
return {
...state,
basket: state.basket.concat("44")
}
Having said that, you should really be using our official Redux Toolkit package, which will let you drastically simplify your reducer logic and catch mistakes like this.

Redux reducer does not force a rerender on my React Component although I mutated the state

I have been struggling with redux lately because it often does not let my React Components rerender. I know that I have to mutate the state in order to let redux know that my state changed. But for some reason, my Redux still doesn't trigger the componentDidUpdate() function on my React Component.
Here is the code for my reducer function:
case ADD_OR_UPDATE_EVENT_OF_MATCH: {
const matches = [...state.matches];
const foundMatch = matches.find((match) => match.matchId === action.matchId);
const foundMatchIndex = matches.findIndex((match) => match.matchId === action.matchId);
if (!foundMatch) return state;
if (foundMatch.events) {
const foundEventIndex = foundMatch.events?.findIndex((event) => event.eventId === action.event.eventId);
if (foundEventIndex === -1) {
foundMatch.events?.push(action.event);
} else {
foundMatch.events[foundEventIndex] = action.event;
}
} else {
foundMatch.events = [action.event];
}
matches[foundMatchIndex] = foundMatch;
if (state.currentMatch) {
if (foundMatch.matchId === state.currentMatch.matchId) {
return {
...state,
currentMatch: foundMatch,
matches: matches
};
} else {
return {
...state,
matches: matches
};
}
}
return state;
}
I'm afraid this statement is very wrong:
I know that I have to mutate the state in order to let redux know that my state changed.
The exact opposite is true. You should never actually mutate Redux state!
Lines like this are mutating, and are therefore breaking your app:
foundMatch.events[foundEventIndex] = action.event;
You need to change this logic to all be immutable updates, instead.
You should also probably look at using our official Redux Toolkit package to write your Redux logic, as it does allow you to write "mutating" logic in reducers that gets turned into safe and correct immutable updates internally.

Redux initial state gets mutated even when using Object.assign

This is a simple replication of a problem i encounter in an actual app.
https://jsfiddle.net/zqb7mf61/
Basically, if you clicked on 'Update Todo" button, the text will change from "Clean Room" to "Get Milk". "Clean Room" is a value in the initial State of the reducer. Then in my React Component, I actually try to clone the state and mutate the clone to change the value to "Get Milk" (Line 35/36). Surprisingly, the initial State itself is also mutated even though I try not to mutate it (as seen in line 13 too).
I am wondering why Object.assign does not work for redux.
Here are the codes from the jsFiddle.
REDUX
const initState = {
task: {id: 1, text: 'Clean Room'}
}
// REDUCER
function todoReducer (state = initState, action) {
switch (action.type) {
case 'UPDATE_TODO':
console.log(state)
let newTodo = Object.assign({}, state) // here i'm trying to not make any changes. But i am surpise that state is already mutated.
return newTodo
default:
return state;
}
}
// ACTION CREATORS:
function updateTodo () {
return {type: 'UPDATE_TODO'};
}
// Create Store
var todoStore = Redux.createStore(todoReducer);
REACT COMPONENT
//REACT COMPONENT
class App extends React.Component{
_onSubmit = (e)=> {
e.preventDefault();
let newTodos = Object.assign({}, this.props.todos) // here i clone the redux state so that it will not be mutated, but i am surprise that it is mutated and affected the reducer.
newTodos.task.text = 'Get Milk'
console.log(this.props.todos)
this.props.updateTodo();
}
render(){
return (
<div>
<h3>Todo List:</h3>
<p> {this.props.todos.task.text} </p>
<form onSubmit={this._onSubmit} ref='form'>
<input type='submit' value='Update Todo' />
</form>
</div>
);
}
}
// Map state and dispatch to props
function mapStateToProps (state) {
return {
todos: state
};
}
function mapDispatchToProps (dispatch) {
return Redux.bindActionCreators({
updateTodo: updateTodo
}, dispatch);
}
// CONNECT TO REDUX STORE
var AppContainer = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(App);
You use Object.assign in both the reducer as in the component. This function only copies the first level of variables within the object. You will get a new main object, but the references to the objects on the 2nd depth are still the same.
E.g. you just copy the reference to the task object around instead of actually creating a new task object.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Deep_Clone
Apart from that it would be better to not load the whole state into your component and handle actions differently. Lets just solve this for now. You will have to create a new task object in your onSubmit instead of assigning a new text to the object reference. This would look like this:
newTodos.task = Object.assign({}, newTodos.task, {text: 'Get Milk'})
Furthermore to actually update the store, you will have to edit your reducer as you now assign the current state to the new state. This new line would look like this:
let newTodo = Object.assign({}, action.todos)

Add logic to the store?

I have a redux application with a "campaign" reducer/store.
Currently I have repeated code to check if a specific campaign is loaded or needs an API call to fetch details from the DB. Much simplified it looks like this:
// Reducer ----------
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
items: {... campaignList}
}
}
// Component ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Now I don't like to repeat the complex condition for needFetch. I also don't like to have this complex code in the mapStateToProps function at all, I want to have a simple check. So I came up with this solution:
// Reducer NEW ----------
const needFetch = (items) => (id) => { // <-- Added this function.
if (!items[id]) return true;
if (!items[id].meta) return true;
if (!items[id].meta.loaded && !items[id].meta.loading) return true;
return false;
}
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
needFetch: needFetch(campaignList), // <-- Added public access to the new function.
items: {... campaignList}
}
}
// Component NEW ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: state.campaign.needFetch(campaignId), // <-- Much simpler!
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Question: Is this a good solution, or does the redux-structure expect a different pattern to solve this?
Question 2: Should we add getter methods to the store, like store.campaign.getItem(myId) to add sanitation (make sure myId exists and is loaded, ..) or is there a different approach for this in redux?
Usually computational components should be responsible for doing this type of logic. Sure your function has a complex conditional check, it belongs exactly inside your computational component (just like the way you currently have it).
Also, redux is only for maintaining state. There's no reason to add methods to query values of the current state inside your reducers. A better way would be having a module specifically for parsing your state. You can then pass state to the module and it would extract the relevant info. Keep your redux/store code focused on computing a state only.
Your approach is somewhat against the idiomatic understanding of state in redux. You should keep only serializable data in the state, not functions. Otherwise you loose many of the benefits of redux, e.g. that you can very easily stash your application's state into the local storage or hydrate it from the server to resume previous sessions.
Instead, I would extract the condition into a separate library file and import it into the container component where necessary:
// needsFetch.js
export default function needsFetch(campaign) {
return campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading);
}
// Component ----------
import needsFetch from './needsFetch';
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: needsFetch(campaign),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);

Categories

Resources