Redux dispatch does not update state array with new payload object - reactjs

I decided to add Redux to my pet project (surprise, todolist).
Here's add entry function:
const [todoEntry, setTodoEntry] = useState('');
const addNewEntry = (e) => {
e.preventDefault();
// console.log(todoEntry);
dispatch({
type: ADD_TODO,
payload: {
prodName: todoEntry,
done: false,
favorite: false,
edit: false,
id: uuid()
}
})
setTodoEntry('');
todoEntry comes from another component like that:
<input
id='standartInput'
style={{minWidth: '250px'}}
value={todoEntry}
onChange={e => setTodoEntry(e.target.value)}
type='text'
placeholder='Add new entry (max 55 symbols)' />
Also, I use some hooks to manage my state:
const myTodoItems = useSelector((state) => state.todos[0])
const dispatch = useDispatch()
const [data, setData] = useState(myTodoItems);
And, finally, the reducer:
import { todolist } from "./todolist"
import { ADD_TODO } from '../Store/todoactions'
export const todoReducer = (state = [todolist], action) => {
switch (action.type) {
case ADD_TODO: {
const newItem = action.payload
console.log(newItem)
console.log(todolist)
return ([...todolist, newItem])
}
default:
{ return state }
}
}
The issue is:
todolist exists, I can see at browser console
newItem exists too, I also can see at browser console
BUT! When clicking on 'Add' button, state is not updated.
What I'm doing wrong?
Thanks.

const myTodoItems = useSelector((state) => state.todos[0])
You seem to be selecting only the first item, so it's not surprising that you don't see the rest.
const [data, setData] = useState(myTodoItems);
This looks like an antipattern, why do you need a state variable for something that is already tracked by Redux?
You should also use Redux Toolkit, it is not recommended to use Redux directly.
Edit:
Thank you for the codesandbox, now the problem is clear.
You are using both Redux and React state to deal with the same data, for instance you add todos through Redux but complete them with React state.
A very important principle in React/Redux is to have a single source of truth, but in your case you have two sources of truth for the todos: the Redux store and the useState hook. You use the React state for rendering and initialize it with the Redux state, but then you don't update it when a todo is added, so the UI shows outdated information.
It's fine to use sometimes Redux, sometimes useState as long as it is for independent pieces of data, but for the same data, you need to choose.
Remember that everytime you use useState(initialState) you create a new state variable/source of truth, which will become different from the initial state. Sometimes this is exactly what you want, but not here.
So I would suggest to remove the useState and go through Redux for everything you want to change about the todos (edit them, complete them, and so on).
There are some things you can have as React state (for instance the boolean "are we currently editing this specific todo item"), but then it would be much easier to have a simple useState(false) directly in the TodoItem component.

Related

Is there a setState in Redux RTK?

I am new to Redux RTK and I am kinda confused. Is there a setState using method like in useState of React?
For example I have this code where I fetch some data from an API, and after I create some extraReducers.
But I am wondering, I have a handleChange of a dropdown, but so far I have not figured out how to setState onChange:
export const getPlayers = createAsyncThunk<IPlayerProps[]>('players/getPlayers', async (_, _thunkApi) => {
try {
const response = await axios.get(
'https://6360055fca0fe3c21aaacc04.mockapi.io/player'
);
return response.data;
} catch (error) {
return _thunkApi.rejectWithValue(error);
}
Here is the Slicer:
export const playerSlice = createSlice({
name: 'players',
initialState,
reducers: {
setPlayers: (state, action: PayloadAction<IPlayerProps[]>) => {
state.players = action.payload;
}
},
extraReducers: (builder) => {
builder.addCase(getPlayers.pending, (state) => {
state.loading = true;
});
builder.addCase(getPlayers.fulfilled, (state, action) => {
state.players = action.payload;
state.loading = false;
});
builder.addCase(getPlayers.rejected, (state, action) => {
state.loading = false;
state.errors = action.payload;
})
}
});
export default playerSlice.reducer;
export const { setPlayers } = playerSlice.actions;
And here is where I do a simple setState with target's value:
const handleChange = (e: SelectChangeEvent<string>) => {
setState(e.target.value);
};
Any help would be appreciated! :)
If I understood your question properly, useState is used to save or update data locally in the same file and the data saved won't be accessible globally in the project.
but using redux, All the project will have access to a shared data storage.
Therefore, Redux work in a different way than useState hook of react.
Thus, to update, change, remove or add values in redux, we use dispatch to call the action and force the redux state update.
for example:
let's say you have an action called getPlayers in redux, so the this action when you call it, it should return data based on a specific type and safe it in the redux reducer state.
So, to call that action you should import useDispatch and use it which will fire the redux action, and you should import useSelector which get the data from redux (in other words, whenever you fire an action in redux and it updates redux state, the useSelector will automatically change the data in the file you are using, so the behavior will be the same as setState):
import { useDispatch } from 'react-redux';
//to get the data from redux
import { useSelector } from 'react-redux';
...
//to fire actions in redux which will force update the redux state (selectors)
const dispatch =useDispatch();
useEffect(()=>{
//this will call the action
dispatch(getPlayers());
},[]);
// when you dispatch(getPlayers()) and the new data is fetched, the players in redux will automatically update.
const players= useSelector((state) => state.players);
...
To conclude, if you want to update a variable on click, you should create an action in redux that will update a variable in the state of redux, which can be called using dispatch and update the variable as a useState behavior.
I know this may sound complicated but once you get the architecture, redux can be very helpful in many scenarios, good luck! and let me know if you need anything or if you have any question.
Here is a quick article that should help you understand redux good architecture (Best Redux architecture explained in 5 minutes

Using the Context API as a way of mimicking useSelector and useDispatch with redux v5

I'm working on a React project where I'm constrained to using React Redux v5, which doesn't include useDispatch and useSelector.
Nonetheless I really would like to have these hooks (or something like them) available in my app.
Therefore, I've created a wrapper component at the top level of the app which I connect using redux's connect function.
My mapStateToProps and mapDispatchToProps then just look like this:
const mapDispatchToProps = (dispatch: DispatchType) => {
return {
dispatch,
};
};
const mapStateToProps = (state: StateType) => {
return {
state,
};
};
export default connect(mapStateToProps, mapDispatchToProps)(MainLayout);
In my wrapper component, I then pass the dispatch and the state into the value:
<DispatchContext.Provider value={{ state, dispatch }}>
{children}
</DispatchContext.Provider>
Finally, I have a hook that looks like this:
const useSelectAndDispatch = () => {
const context = useContext(DispatchContext);
if (context === null) {
throw new Error("Please use useDispatch within DispatchContext");
}
const { state, dispatch } = context;
function select(selector) {
return selector(state);
}
return { dispatch, select };
};
I then use dispatch and selector in my components via useSelectAndDispatch.
I was wondering if this is an appropriate way to go about this issue, and whether I can expect any performance problems. I am using reselect, and have a good understanding of memoization. I'm just looking for opinions, since I've heard that the redux team held off implementing useDispatch and useSelector for a long time because of performance issues.
Many thanks for any opinions!
This will cause significant peformance problems. Your mapStateToProps is using the entire state object, so every time anything changes in the state, the provider must rerender. And since the provider rerendered with a new value, so too must every component that consumes the context. In short, you will be forcing most of your app to rerender anytime anything changes.
Instead of using mapStateToProps and mapDispatchToProps, i would go back to the actual store object, and build your hooks from that. Somewhere in your app is presumably a line of code that says const store = createStore(/* some options */).
Using that store variable, you can then make some hooks. If i can assume that there's only one store in your app, then the dispatch hook is trivial:
import { store } from 'wherever the store is created'
export const useMyDispatch = () => {
return store.dispatch;
}
And the selector one would be something like this. It uses .subscribe to be notified when something in the store changes, and then it uses the selector to pluck out the part of the state that you care about. If nothing changed, then the old state and new state will pass an === check, and react skips rendering. If it has changed though, the component renders (only the component that called useMySelect plus its children, not the entire app)
export const useMySelector = (selector) => {
const [value, setValue] = useState(() => {
return selector(store.getState());
});
useEffect(() => {
const unsubscribe = store.subscribe(() => {
const newValue = selector(store.getState());
setValue(newValue);
});
return unsubscribe;
}, []);
return value;
}

Special actions on React state variables

I have a React component with a state variable that needs specific actions. For example, consider a component that shows a list of user profiles, and the user can switch to another profile or create a new one. The state variable is a list of user profiles, and a second variable is the currently selected profile; the component can add a new profile (which is more specific than just "setting" a new list of profiles), or it can change the currently selected profile.
My first idea was to have two useState hooks, one for the list and one for the current profile. However, one problem with that is that I would like to store the current profile's id, which refers to one of the profiles in the list, which means that the two state variables are inter-dependent. Another issue is that having a generic setProfiles state change function is a bit too "open" for my taste: the add logic may be very specific and I would like to encapsulate it.
So I came up with this solution: a custom hook managing the two state variables and their setters, that would expose the two values (list and current id) and their appropriate actions (add new profile and select profile).
This is the code of the hook:
export const useProfileData = () => {
const [ profiles, setProfiles ] = useState([]);
const [ currentProfileID, setCurrentProfileID ] = useState(null);
const [ currentProfile, setCurrentProfile ] = useState(null);
useEffect(() => {
// This is actually a lazy deferred data fetch, but I'm simplifying for the sake of brevity
setProfiles(DataManager.getProfiles() || [])
}, [])
useEffect(() => {
if (!profiles) {
setCurrentProfile(null);
return;
}
const cp = profiles.find(p => p.ID === currentProfileID);
setCurrentProfile(cp);
}, [ currentProfileID, profiles ])
return {
currentProfile: currentProfile,
profiles: profiles,
setCurrentProfileID: i_id => setCurrentProfileID(i_id),
addNewProfile: i_profile => {
profiles.push(i_profile);
setProfiles(profiles);
DataManager.addNewProfile(i_profile); // this could be fire-and-forget
},
};
};
Three states are used: the list, the current profile id and the current profile (as an object). The list is retrieved at mounting (the current id should be too, but I omitted that for brevity). The current profile is never set directly from the outside: the only way to change it is to change the id or the list, which is managed by the second useEffect. And the only way to change the id is through the exposed setCurrentProfileID function.
Adding a new profile is managed by an exposed addNewProfile function, that should add the new profile to the list in state, update the list in state, and add the new profile in the persistent DataManager.
My first question is: is it ok to design a hook like this? From a general software design point of view, this code gives encapsulation, separation of concerns, and a correct state management. What I'm not sure about if this is proper in a functional world like React.
My second question is: why is my component (that uses useProfileData) not updated when addNewProfile is called? For example:
const ProfileSelector = (props) => {
const [ newProfileName, setNewProfileName ] = useState('');
const { profiles, currentProfile, setCurrentProfileID, addNewProfile } = useProfileData();
function createNewProfile() {
addNewProfile({
name: newProfileName,
});
}
return (
<div>
<ProfilesList profiles={profiles} onProfileClick={pid => setCurrentProfileID(pid)} />
<div>
<input type="text" value={newProfileName} onChange={e => setNewProfileName(e.target.value)} />
<Button label="New profile" onPress={() => createNewProfile()} />
</div>
</div>
);
};
ProfilesList and Button are components defined elsewhere.
When I click on the Button, a new profile is added to the persistent DataManager, but profiles is not updated, and ProfilesList isn't either (of course).
I'm either implementing something wrong, or this is not a paradigm that can work in React. What can I do?
EDIT
As suggested by #thedude, I tried using a reducer. Here is the (stub) of my reducer:
const ProfilesReducer = (state, action) => {
const newState = state;
switch (action.type) {
case 'addNewProfile':
{
const newProfile = action.newProfile;
newState.profiles.push(newProfile);
DataManager.addNewProfile(newProfile);
}
break;
default:
throw new Error('Unexpected action type: ' + action.type);
}
return newState;
}
After I invoke it (profilesDispatch({ type: 'addNewProfile', newProfile: { name: 'Test' } });), no change in profilesState.profiles is detected - or at least, a render is never triggered, nor an effect. However, the call to DataManager has done its job and the new profile has been persisted.
You should never mutate your state, not even in a reducer function.
From the docs:
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
Change your reducer to return a new object:
const ProfilesReducer = (state, action) => {
switch (action.type) {
case 'addNewProfile':
{
const newProfile = action.newProfile;
return {...state, profiles: [...state.profiles, newProfile]}
}
break;
default:
throw new Error('Unexpected action type: ' + action.type);
}
return state;
}
Also not that reducer should no have side effects, if you want to perform some action based on a state change, use a useEffect hook for that.
For example:
DataManager.addNewProfile(newProfile) should not be called from the reducer

Redux useSelector with id field

I need your advice on filtering data with a selector. Suppose I have the following entities in my application. 1 organization has multiple devices which look the following in my state shape:
state {
devices: {
byId: [ 1 { name: device1 } ]
}
organizations: {
byId: [
1 { name: org1, devices: [1,2,3] }
]
}
}
Now I want to filter the devices inside the organization. This is something that I want to do with a selector. My selector looks like the following:
const selectDevice = (id: any) => (state: State) => state.devices.byId[id]
export const selectOrganizationDevices = (id: number) => (state: State) => {
const organization = state.organizations.byId[id] || []
return organization.devices.map(id => selectDevice(id)(state))
}
This should be working fine but my selector got called before I have dispatched the data from redux. I suppose that there's something wrong with my reducer or the component I've created.
My reducer looks like this:
return produce(state, draftState => {
switch (action.type) {
...
case OrganizationActionTypes.FETCH_DEVICES_SUCCESS: {
draftState.byId = action.payload.entities.organizations
draftState.allIds = Object.keys(action.payload.entities.organizations)
draftState.loading = false
break;
}
...
default: {
return state
}
}
})
My functional component looks like the following:
function Devices() {
const dispatch = useDispatch();
const devices = useSelector(selectOrganizationDevices(1))
useEffect(() => {
dispatch(fetchOrganizationDevices(1))
}, [])
const columns = []
return (
<Layout headerName={"Devices"}>
<Table dataSource={devices} columns={columns}/>
</Layout>
)
}
The error I get now is organization.devices is undefined which says that the array of devices in the state is empty. It seems that useSelector is called before dispatch. How can I prevent redux of doing this? Or what should be changed in my code?
Yes, the useEffect hook runs after the first render. useSelector will run during the first render. So, your component code needs to safely handle the case where that data doesn't exist yet.
Also, don't put a hardcoded array/object literal in a selector like that, as it will be a new reference every time and force your component to re-render every time an action is dispatched. Either extract that to a constant outside of the selector, or use a memoization library like Reselect to create the selector.
Finally, you should be using our official Redux Toolkit package, which includes utilities to simplify several common Redux use cases, including store setup, defining reducers, immutable update logic, and even creating entire "slices" of state at once. It also has a new createEntityAdapter API that helps you manage normalized state in the store.

Is it normal that the component gets rendered twice when two state variables are updated?

In my test app, every time a certain function is called, a call to my API is made (axios.get) and then two state variables are updated with the data received from the database. These two state variables both change a part of what is shown on the screen.
The things is, I added a useEffect hook to "debug" the amount of re-renders and I noticed that the component is re-rendered twice, I guess because it is once for one state variable and once for the other one. I thought using useReducer would change this, but it doesn't.
Is this a normal React behaviour or is there something I should be doing differently in order for the component is re-rendered only once?
Edit: I am editing to add the code:
(It's a trivia kind of test app, I'm new to React, so I am practicing)
import React, { useEffect, useReducer } from 'react'
import axios from 'axios'
import './App.css';
import reducer from './Reducer.js'
const initialState = {
question: '',
id: 1,
choices: []
}
const Questions = () => {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
console.log('executed');
})
const getQuestion = async (e) => {
try {
e.preventDefault()
const res = await axios.get(`/questions/${state.id}`)
dispatch({
type: 'set_question',
payload:
res.data.question
})
dispatch({
type: 'set_choices',
payload:
[res.data.correct_answer,
res.data.incorrect_answer1,
res.data.incorrect_answer2]
})
} catch (err) {
console.log(err);
}
}
return (
<div>
<form onSubmit={getQuestion}>
<button>Get next question</button>
</form>
<h1> {state.question ? state.question : null}</h1>
<button> {state.choices ? `${state.choices[0]}` : null}</button>
<button> {state.choices ? ` ${state.choices[1]}` : null}</button>
<button> {state.choices ? ` ${state.choices[2]}` : null}</button>
</div>
)
}
export default Questions
Reducer:
const reducer = (state, action) => {
switch (action.type) {
case 'set_question':
return {
...state,
question: action.payload
}
case 'set_choices':
return {
...state,
choices: action.payload
}
default:
return state
}
}
export default reducer
React only batches state updates in event handlers and lifecycle methods. If the state updates happen in an async function e.g. in response of a successful call to fetch or a setTimeout they will not be batched. This is announced to change in a future version of react.
Also see this answer from Dan Abramov about this:
https://stackoverflow.com/a/48610973/5005177
However, both in React 16 and earlier versions, there is yet no batching by default outside of React event handlers. So if in your example we had an AJAX response handler instead of handleClick, each setState() would be processed immediately as it happens. In this case, yes, you would see an intermediate state.
promise.then(() => {
// We're not in an event handler, so these are flushed separately.
this.setState({a: true}); // Re-renders with {a: true, b: false }
this.setState({b: true}); // Re-renders with {a: true, b: true }
this.props.setParentState(); // Re-renders the parent
});
If you want your component to re-render only once you have to keep all the data in a single state object so that you only have to call setState once (or dispatch if you want to use useReducer) with the new data.
Another workaround is to wrap your block of state updates in ReactDOM.unstable_batchedUpdates(() => {...}), which will most likely not be required anymore in a future version of react. Also see the answer of Dan Abramov from above for details.

Resources