Is there a setState in Redux RTK? - reactjs

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

Related

Redux dispatch does not update state array with new payload object

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.

Unable to Update State ReduxToolkit

I have a array of objects kept in my state, I want to be able to edit one of the objects in the array and update the state.
However, I cannot seem to update anything with the state except push more items into it.
I am using #reduxjs/toolkit and the createSlice() method for my reducers.
Here is my slice, it has some logic to pull the initial state array from an API.
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import { BACKEND_API } from "../../utilities/environment";
import fetchViaApi from "../../utilities/fetchViaApi";
export const getInitialDashboards = createAsyncThunk(
'dashboard/getDashboards',
async () => {
const response = await fetchViaApi('/dashboards', {
baseUrl: BACKEND_API,
method: "GET"
});
const data = await response.json();
return data;
}
)
const initialState = [];
const dashboardsSlice = createSlice({
name: 'dashboards',
initialState,
reducers: {
setDashboards: (state,action) => {
state = action.payload;
},
updateDashboard: (state,action) => {
// state.push(action.payload);
state = [...state.slice(0, 5)];
},
deleteDashboard: (state, action) => {
},
},
extraReducers: builder => {
builder.addCase(getInitialDashboards.fulfilled, (state, action) => {
action.payload.forEach(element => {
state.push(element);
});
})
}
});
export const { setDashboards, updateDashboard, editDashboard, deleteDashboard } = dashboardsSlice.actions;
export default dashboardsSlice.reducer;
The commented out state.push(action.payload) works fine, but sometimes I don't want to add new object to the array, but edit existing ones.
My thought was to slice the existing element out and add the new version back to the array. But I cannot slice the state.
I am using Redux DevTools in Chrome and watching the state not change after calling updateDashboard, there were 10 elements after getDashboards is completed.
You had the right idea, but your reducers need to be returning the new state, not assigning it.. e.g.
reducers: {
setDashboards: (state,action) => {
return action.payload;
},
updateDashboard: (state,action) => {
return [...state.slice(0, 5)];
},
deleteDashboard: (state, action) => {
return [];
},
},
The issue is that state = anything is not a valid way to update data with Immer. It's not mutating the existing state, and it's not returning a new value - it just points the local state variable to something else, so Immer has no way to know that anything changed.
If you want to replace the existing state entirely, do return newStateValue. If you want to update part of the state, then mutate a nested field or value.
See the Writing Reducers with Immer page in the RTK docs for more details.
I faced a similar problem today. Updating or assigning values to the state directly is not working. But updating the properties inside the state variable works
I would add a property named dashboards to the state and update it instead of updating the state directly in reducer
Redux toolkit is using immer under the hood. It might be helpful to take a look at immer and get an idea to mutate the state

React-Redux: how to set the state?

I am trying to understand someone else their code but have difficulty understand the interaction between Redux and React.
On a React page, I invoke a Redux action called getSubscriptionPlan. Inside that Redux action, I see it is able to load the correct data (point 1 below). This uses a reducer, in which I can again confirm the correct data is there (point 2 below).
Then the logic returns to the React page (point 3 below). I now would expect to be able to find somewhere in the Redux store the previously mentioned data. However, I can't find that data listed anywhere... not in this.state (where I would expect it), nor in this.props. Did the reducer perhaps not update the store state...?
What am I doing wrong and how can I get the data to point 3 below?
React page:
import { connect } from "react-redux";
import { getSubscriptionPlan } from "../../../appRedux/actions/planAction";
async componentDidMount() {
let { planId } = this.state;
await this.props.getSubscriptionPlan(planId);
// 3. I can't find the data anywhere here: not inside this.state and not inside this.props.
this.setState({plan: this.state.plan});
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.payment.paymentData !== this.props.payment.paymentData) {
this.setState({
checkout: this.props.payment.paymentData,
plan: this.props.payment.paymentData.plan,
});
}
}
const mapStateToProps = (state) => {
return {
plan: state.plan,
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators(
{ getSubscriptionPlan }, dispatch
);
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Checkout)
);
Redux action:
export const getSubscriptionPlan = (id) => {
let token = getAuthToken();
return (dispatch) => {
axios
.get(`${url}/getSubscriptionPlan/${id}`, {
headers: { Authorization: `${token}` },
})
.then((res) => {
if (res.status === 200) {
// 1. From console.log(res.data) I know res.data correctly now contains the data
return dispatch({
type: GET_PLAN_SUCCESS,
payload: res.data,
});
})
};
};
Reducer:
export default function planReducer(state = initial_state, action) {
switch (action.type) {
case GET_PLAN_SUCCESS:
// 2. I know action.payload, at this point contains the correct data.
return { ...state, plan: action.payload };
default:
return state;
}
}
You are getting tripped up on how Redux works.
Redux does not use react component state. It manages state separately, and passes that state to components as props. When you call getSubscriptionPlan, you asynchronously dispatch an event to Redux, which handles the event and updates store state in the reducer. This state is the passed to the connected components mapStateToProps function, mapped to props, and then passed as props to your component. Passing new props triggers a componentDidUpdate and a rerender of the component.
A few key things here.
Redux does not interact with component state unless you explicitly set state with props passed from Redux.
Redux is asynchronous. That means that when you make a change to state via dispatch, the change is not immediately available in the component, but only available when new props are passed. It's event driven, not data binding. As a result, in your code you woun't see the plan prop in componentDidMount because at the time componentDidMount the call to getSubscriptionPlan hasn't happened.
You should see the prop populated in this.props in componentDidUpdate and in render before the didUpdate.
When working with react, it's best to think of components as basically functions of props with some extra lifecycle methods attached.

How do I manually dispatch actions to a store created with configureStore?

I have a project where half of it was made with classes, and the other half is being made with hooks and Redux.
For this, I have created a store with configureStore() from Redux Toolkit and provided it using the Provider component. In a very minimal way, the store is set up as follows:
const userSlice = createSlice({
name: 'user',
initialState: {
user: {}
},
reducers: {
validate: (state, action) => state.user = action.payload
}
})
const store configureStore({
reducer: {
user: userSlice.reducer
}
})
There are two components - a new one, functional, which uses the useSelector() hook, and an older one, which is class based, but needs to use this sasme store to dispatch an action.
To do this, I import the store and fire
store.dispatch({type: 'user/validate', payload: newUser});
from the class component.
I receive no errors, yet nothing happens at all.
I tracked my input from DevTools' Redux plugin, and I can see the state does not change, so I assume my manual call to dispatch is somehow wrong.
What I expect to happen is for the state to update, which would trigger a re-render of the component that uses useSelector
The following way is a safe way to dispatch actions without misspelling the type string.
Extract the action from reducer
const userSlice = createSlice({
name: 'user',
initialState: {
user: {}
},
reducers: {
validate: (state, action) => {
state.user = action.payload
}
}
})
// <------------------
// Action creators are generated for each case reducer function
export const { validate } = userSlice.actions
export const store = configureStore({
reducer: {
user: userSlice.reducer
}
})
Dispatch the action as so
store.dispatch(validate(newUser))
Issue
I'm going to say the issue is that you are trying to both mutate your state object in the reducer function and return it.
See Mutating and Returning State
In any given case reducer, Immer expects that you will either mutate
the existing state, or construct a new state value yourself and return
it, but not both in the same function!
Solution
Just mutate the state, don't return it.
reducers: {
validate: (state, action) => {
state.user = action.payload;
},
}
If you want your class-based component to subscribe to your redux store then you can still use the connect Higher Order Component from react-redux.
Example:
import { connect } from 'react-redux';
import { validate } from '../path/to/userSlice';
class MyComponent extends Component {
...
// In a function you can simply dispatch the validate action
// as it was wrapped in a call to dispatch already and injected
// as a prop.
this.props.validate(somePayloadValue);
...
}
const mapDispatchToProps = {
validate
};
export default connect(null, mapDispatchToProps)(MyComponent);

How can I cache data that I already requested and access it from the store using React and Redux Toolkit

How can I get data from the store using React Redux Toolkit and get a cached version if I already requested it?
I need to request multiple users for example user1, user2, and user3. If I make a request for user1 after it has already been requested then I do not want to fetch user1 from the API again. Instead it should give me the info of the user1 from the store.
How can I do this in React with a Redux Toolkit slice?
Edit: This answer predates the release of RTK Query which has made this task much easier! RTK Query automatically handles caching and much more. Check out the docs for how to set it up.
Keep reading if you are interested in understanding more about some of the concepts at play.
Tools
Redux Toolkit can help with this but we need to combine various "tools" in the toolkit.
createEntityAdapter allows us to store and select entities like a user object in a structured way based on a unique ID.
createAsyncThunk will create the thunk action that fetches data from the API.
createSlice or createReducer creates our reducer.
React vs. Redux
We are going to create a useUser custom React hook to load a user by id.
We will need to use separate hooks in our hooks/components for reading the data (useSelector) and initiating a fetch (useDispatch). Storing the user state will always be the job of Redux. Beyond that, there is some leeway in terms of whether we handle certain logic in React or in Redux.
We could look at the selected value of user in the custom hook and only dispatch the requestUser action if user is undefined. Or we could dispatch requestUser all the time and have the requestUser thunk check to see if it needs to do the fetch using the condition setting of createAsyncThunk.
Basic Approach
Our naïve approach just checks if the user already exists in the state. We don't know if any other requests for this user are already pending.
Let's assume that you have some function which takes an id and fetches the user:
const fetchUser = async (userId) => {
const res = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`);
return res.data;
};
We create a userAdapter helper:
const userAdapter = createEntityAdapter();
// needs to know the location of this slice in the state
export const userSelectors = userAdapter.getSelectors((state) => state.users);
export const { selectById: selectUserById } = userSelectors;
We create a requestUser thunk action creator that only executes the fetch if the user is not already loaded:
export const requestUser = createAsyncThunk("user/fetchById",
// call some API function
async (userId) => {
return await fetchUser(userId);
}, {
// return false to cancel
condition: (userId, { getState }) => {
const existing = selectUserById(getState(), userId);
return !existing;
}
}
);
We can use createSlice to create the reducer. The userAdapter helps us update the state.
const userSlice = createSlice({
name: "users",
initialState: userAdapter.getInitialState(),
reducers: {
// we don't need this, but you could add other actions here
},
extraReducers: (builder) => {
builder.addCase(requestUser.fulfilled, (state, action) => {
userAdapter.upsertOne(state, action.payload);
});
}
});
export const userReducer = userSlice.reducer;
But since our reducers property is empty, we could just as well use createReducer:
export const userReducer = createReducer(
userAdapter.getInitialState(),
(builder) => {
builder.addCase(requestUser.fulfilled, (state, action) => {
userAdapter.upsertOne(state, action.payload);
});
}
)
Our React hook returns the value from the selector, but also triggers a dispatch with a useEffect:
export const useUser = (userId: EntityId): User | undefined => {
// initiate the fetch inside a useEffect
const dispatch = useDispatch();
useEffect(
() => {
dispatch(requestUser(userId));
},
// runs once per hook or if userId changes
[dispatch, userId]
);
// get the value from the selector
return useSelector((state) => selectUserById(state, userId));
};
isLoading
The previous approach ignored the fetch if the user was already loaded, but what about if it is already loading? We could have multiple fetches for the same user occurring simultaneously.
Our state needs to store the fetch status of each user in order to fix this problem. In the docs example we can see that they store a keyed object of statuses alongside the user entities (you could also store the status as part of the entity).
We need to add an empty status dictionary as a property on our initialState:
const initialState = {
...userAdapter.getInitialState(),
status: {}
};
We need to update the status in response to all three requestUser actions. We can get the userId that the thunk was called with by looking at the meta.arg property of the action:
export const userReducer = createReducer(
initialState,
(builder) => {
builder.addCase(requestUser.pending, (state, action) => {
state.status[action.meta.arg] = 'pending';
});
builder.addCase(requestUser.fulfilled, (state, action) => {
state.status[action.meta.arg] = 'fulfilled';
userAdapter.upsertOne(state, action.payload);
});
builder.addCase(requestUser.rejected, (state, action) => {
state.status[action.meta.arg] = 'rejected';
});
}
);
We can select a status from the state by id:
export const selectUserStatusById = (state, userId) => state.users.status[userId];
Our thunk should look at the status when determining if it should fetch from the API. We do not want to load if it is already 'pending' or 'fulfilled'. We will load if it is 'rejected' or undefined:
export const requestUser = createAsyncThunk("user/fetchById",
// call some API function
async (userId) => {
return await fetchUser(userId);
}, {
// return false to cancel
condition: (userId, { getState }) => {
const status = selectUserStatusById(getState(), userId);
return status !== "fulfilled" && status !== "pending";
}
}
);

Resources