This is my state shape:
export default {
app: {
loading: false,
loadError: null
},
search: {
type: null,
text: ''
},
things: {
byId: {
},
allIds: []
}
}
When I fetch new things from the server, I want to update both things.byId and things.allIds - as recommended by Redux docs. I'm doing it in the following way but I feel like there's a better way:
import { LOAD_THINGS_SUCCESS } from '../actions/actionTypes'
import initialState from './initialState'
export default function things(state = initialState.things, action) {
switch(action.type) {
case LOAD_THINGS_SUCCESS:
const thingsById = {}
action.things.forEach(thing => {
thingsById[thing.id] = thing
})
return {
...state,
byId: thingsById,
allIds: action.things.map(thing => thing.id)
}
default:
return state
}
}
I'm using combineReducers and the above reducers/things.js is one of them. The part I'm concerned about is action.things.forEach... I have a feeling there's a cleaner / more efficient way of doing this.
Related
I am working on a application which uses redux for state management. There, at a some condition, I want to update the state.
My initial state and reducer function looks like this:
import { createSlice } from '#reduxjs/toolkit';
const filterDataTemplate = {
programId: '',
year: '',
};
const initialState = {
//some other state
filterData: { ...filterDataTemplate },
};
const slice = createSlice({
name: 'editFilterSlice',
initialState: initialState,
reducers: {
updateFilterProgramId: (state, action) => {
state.filterData.programId = action.payload;
},
updateFilterYear: (state, action) => {
state.filterData.year = action.payload;
},
},
});
export const {
updateFilterYear,
updateFilterProgramId,
} = slice.actions;
export default slice.reducer;
So filter details containg year and programId is obtained with the help of this code:
const filterDetails = useAppSelector(
(state) => state.locationsFilter.filterData
);
Let's say I have filter data initially:
filterDetails: {year:2021, programId: "Ameria"}
And i want to have my new filter data to be
filterDetails: {year: "", programId: "Ameria"}
So for this what I am doing:
const handleDelete = (e) => {
e.preventDefault();
if (//some condition) {
console.log("delete is called");
dispatch(updateFilterYear(''));
} else {
dispatch(updateFilterProgramId(''));
}
}
handleDelete function is getting called properly when I am clicking a button because I am getting value inside console.
But after running this code my filter data is not updating. I am not sure what I am doing wrong.
Please help with this.
Action.payload is of object type. So You should reference action.payload.year.
I hope this example will be of any use
setTodoDate: {
reducer: (state, action: PayloadAction<TodoDate>) => {
state.currentDate = action!.payload.date;
},
prepare: (value) => ({
payload: { ...value },
}),
}
I think the issue is because you are trying to mutate your state directly. This is bad practice, and Redux state (and more generally react) is intended to be immutable. Reducers should return a copy of the state, along with the updated values. Documentation linked below.
Redux Documentation
Try writing your reducers like the following
updateFilterYear: (state, action) => {
return {
...state,
filterData: {
...state.filterData,
year: action.payload
}
},
updateFilterProgramId: (state, action) => {
return {
...state,
filterData: {
...state.filterData,
programId: action.payload
}
},
I have a react app that is connected with redux. The component has a form that makes a PUT call to the api when the form is submitted. When I submit the form, I can see that redux gets updated accordingly but when I try to access the redux state as a prop in my component, the props data does not return the current data and is off by 1. For example, here's the data in my redux store:
Redux store:
When I do console.log("THIS PROPS: ", this.props) in my component, I see that it accountError is showing up as null
When I dispatch the action again the second time, only then I see that I am getting the data from redux in my props:
Here is the code that I have currently:
OrgAccount.js
import { registerOrgAccount, getListOfOrgsAndAccts } from "../../store/actions";
handleSubmit = () => {
this.props.registerOrgAccount(this.state)
console.log("THIS PROPS: ", this.props)
if(this.props.accountError === null) {
this.toggleTab(this.state.activeTab + 1);
}
};
<Link
to="#"
className="btn w-lg"
onClick={() => {
if (this.state.activeTab === 1) {
this.handleSubmit();
}
}}
>
Next
</Link>
const mapStatetoProps = (state) => {
const { accounts, accountError, loading } = state.OrgAccount;
return { accounts, accountError, loading };
};
const mapDispatchToProps = (dispatch) => {
return {
getListOfOrgsAndAccts: () => {
dispatch(getListOfOrgsAndAccts())
},
registerOrgAccount: (data) => {
dispatch(registerOrgAccount(data))
},
}
}
export default connect(mapStatetoProps, mapDispatchToProps)(OrgAccount);
Reducer:
const initialState = {
accountError: null, accountsError: null, message: null, loading: null
}
const orgAccount = (state = initialState, action) => {
switch (action.type) {
case REGISTER_ORG_ACCOUNT:
state = {
...state,
account: null,
loading: true,
// accountError: null
}
break;
case REGISTER_ORG_ACCOUNT_SUCCESSFUL:
state = {
...state,
account: action.payload,
loading: false,
accountError: null
}
break;
case REGISTER_ORG_ACCOUNT_FAILED:
state = {
...state,
loading: false,
accountError: action.payload ? action.payload.response : null
}
break;
...
default:
state = { ...state };
break;
}
return state;
}
export default orgAccount;
Action
export const registerOrgAccount = (account) => {
return {
type: REGISTER_ORG_ACCOUNT,
payload: { account }
}
}
export const registerOrgAccountSuccessful = (account) => {
return {
type: REGISTER_ORG_ACCOUNT_SUCCESSFUL,
payload: account
}
}
export const registerOrgAccountFailed = (error) => {
return {
type: REGISTER_ORG_ACCOUNT_FAILED,
payload: error
}
}
Saga.js
import { registerOrgAccountSuccessful, registerOrgAccountFailed, getListOfOrgsAndAcctsSuccessful, getListOfOrgsAndAcctsFailed } from './actions';
import { putOrgAccount } from '../../../helpers/auth_helper';
function* registerOrgAccount({ payload: { account } }) {
try {
const response = yield call(putOrgAccount, {
orgId: account.orgId,
accountNumber: account.accountNumber,
accountName: account.accountName,
accountCode: account.accountCode,
urlLink: account.urlLink,
location: account.location,
accountType: account.accountType,
address: account.address,
city: account.city,
state: account.state,
zip: account.zip,
country: account.country,
email: account.email,
eula: "blah"
});
yield put(registerOrgAccountSuccessful(response));
} catch (error) {
yield put(registerOrgAccountFailed(error));
}
}
To understand the root cause here, I think it helps to know a little about immutability and how React rerenders. In short, React will rerender when it detects reference changes. This is why mutating a prop, wont trigger a rerender.
With that in mind, at the time you call handleSubmit, this.props.accountError is simply a reference to a value somewhere in memory. When you dispatch your action and your state is updated, a new reference will be created, which will trigger a rerender of your component. However the handleSubmit function that was passed to your element still references the old this.props.accountError, which is why it is still null.
You could get around this by implementing your check in the componentDidUpdate lifecycle method. E.g. something like this:
componentDidUpdate(prevProps) {
if (prevProps.accountError === null && this.props.accountError !== null) {
this.toggleTab(this.state.activeTab + 1)
}
}
I have two problems when trying to update my state.
First of all, the first letter in the input is not being updated directly to the state - in the console.log I can see that the useReducer is first calling the initialState and then is dispatching the actions, so the registered input is one letter behind the actual user's input.
Could you please guide me and show me what would be the best way to update the state of my object? I think I should divide the dispatch on more keys, but I feel a little bit lost and don't really know how to dispatch it correctly.
PS of course I dispatched more actions - that work - so I hid some of them, hence the structure of initial state :)
Calling useReducer in Inputs.ts
const [store, dispatch] = useReducer(reducer, initialState);
Actions.ts
export const SET_INPUT_STATE = 'SET_INPUT_STATE';
export const setInputState = (dispatch, payload) => dispatch({ type: SET_INPUT_STATE, payload });
Store.ts
import { SET_INPUT_STATE } from './actions';
export interface StateType {
formState: Record<string, { value: string; isValid: boolean }>;
}
export const initialState: StateType = {
inputState: {
email: {
value: '',
isValid: false,
},
password: {
value: '',
isValid: false,
},
confirmedPassword: {
value: '',
isValid: false,
},
name: {
value: '',
isValid: false,
},
},
};
export const reducer = (state: StateType, action): StateType => {
const { type, payload } = action;
switch (type) {
case SET_FORM_STATE:
return {
...state,
inputState: { ...state.inputState, [payload.name]: { isValid: payload.isValid, value: payload.value } },
};
default:
return state;
}
};
Ok so i'll try to sum it up. I've modified my code to have 2 reducers instead of 1 (for better reading) and i started getting an infinite loop.
The redux props seem to keep updating, however there's no change in them.
This is my reducer file.
import { ADD_LISTITEM } from "../constants/action-types.jsx";
import { SET_SPOTOKEN } from '../constants/action-types.jsx';
import { SET_LISTVIEW } from '../constants/action-types.jsx';
import { CHECK_SPOTOKEN } from '../constants/action-types.jsx';
import { SET_FORMPANEL } from '../constants/action-types.jsx';
import { SET_FORMPANELTYPE } from '../constants/action-types.jsx';
import { combineReducers } from 'redux'
const initialState = {
listItems: [],
spoToken: '',
listView: "All Items",
checkToken: "200",
showNotice: true,
//showFormPanel: false,
//formPanelType: ''
};
const listState = {
showFormPanel: false,
formPanelType: ''
}
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_LISTITEM:
return { ...state, listItems: [...state.listItems, action.payload] };
case SET_SPOTOKEN:
return { ...state, spoToken: action.payload };
case SET_LISTVIEW:
return { ...state, listView: action.payload };
case CHECK_SPOTOKEN:
return { ...state, checkToken: action.payload };
default:
return state;
}
};
const listReducer = (state = listState, action) => {
switch (action.type) {
case SET_FORMPANEL:
return { ...state, showFormPanel: action.payload };
case SET_FORMPANELTYPE:
return { ...state, formPanelType: action.payload };
default:
return state;
}
}
const allReducers = combineReducers({
rootReducer,
listReducer
});
export default allReducers;
Any idea why this might cause a problem?
Edited with Redux State ->
{
root: { … }, newItem: { … }, forms: { … }
} forms: {
$form: { … }, newItem: { … }
} newItem: { Title: "", ItemID: "", OfferingID: "", DeliveryModality: "", Status: "", … }
root: listReducer: {
showFormPanel: false, formPanelType: ""
} rootReducer: {
listItems: Array(4), spoToken: { … }, listView: "All Items", checkToken: "200", showNotice: true
}
Puttin the above into words since it looks kinda messed up.
State contains -> forms, newItem, root. Root contains listReducer and rootReducer.
FIXED: only 1 combineReducers allowed.
Apparently since i didnt have different reducers before adding this one i had another combineReducers in my redux-form file. I assume that was the problem (having 2 combineReducers) because it seems to work fine after modifying it.
I'm setting up actions and reducers in my react-redux app. I need a function to update a property in the state and add objects to its list, if possible with the spread syntax. Here's what I have so far:
const defaultState = {
genres: {}
}
export default function(state = defaultState, action) {
switch(action.type) {
case 'ADD_GENRE':
return {
...state,
genres[action.name]: action.list //new code here
}
default:
return state;
}
}
I need the genres property to be dynamically accessible like an array using its property name like so:
const getMusicFromGenre = (genre) => {
return state.genres[genre];
}
The reducer should accept the following action, then modify the state accordingly:
// action
{
type: 'ADD_GENRE,
name: 'Rock',
list: ['Bohemian Rhapsody', 'Stairway to Heaven', 'Hotel California']
}
// old state
{
genres: {
"Pop": ['Billie Jean', 'Uptown Funk, 'Hey Jude']
}
}
// new state
{
genres: {
"Pop": ['Billie Jean', 'Uptown Funk, 'Hey Jude'],
"Rock": ['Bohemian Rhapsody', 'Stairway to Heaven', 'Hotel California']
}
}
I'm willing to use a different approach if necessary.
You're on the right track, but need to handle each level of nesting separately. Here's an example I wrote for http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html :
function updateVeryNestedField(state, action) {
return {
....state,
first : {
...state.first,
second : {
...state.first.second,
[action.someId] : {
...state.first.second[action.someId],
fourth : action.someValue
}
}
}
}
}
You may also want to read some of the articles on immutable data handling that I have linked at http://redux.js.org/docs/recipes/reducers/PrerequisiteConcepts.html#immutable-data-management and https://github.com/markerikson/react-redux-links/blob/master/immutable-data.md .
immutability-helper is a very useful library for doing state updates. In your situation it would be used like this, which will create a new array if there are no existing items, or concat the existing items with the action's list if there are pre-existing items:
import update from 'immutability-helper';
const defaultState = {
genres: {}
}
const createOrUpdateList = (prev, list) => {
if (!Array.isArray(prev)) {
return list;
}
return prev.concat(list);
// or return [...prev, ...list] if you prefer
}
export default function(state = defaultState, action) {
switch(action.type) {
case 'ADD_GENRE':
return update(state, {
genres: {
[action.name]: {
$apply: prev => createOrUpdate(prev, action.list)
}
}
});
default:
return state;
}
}