I have an object in my redux state
const obj = {
name: 'name',
age: 2,
place: 0,
}
I show these values on the page but I want to make two of them editable so that the object can be updated.
For that I'm basically getting values from two inputs and sending them to my action
export const saveEditedData = data => dispatch => {
dispatch({
type: CHANGE_DATA,
data,
});
}
and then in reducer
case 'CHANGE_DATA':
return {
...state,
obj: {
...state.obj,
name: action.data.name,
age: action.data.age,
}
}
The problem that I'm facing is that if one value is updated and another is not then after this action my second value in empty.
My question is what is a good way to determine which field is changed and update only it?
So far I only came up with putting if else in action to dispatch certain thing. Maybe there is a better way?
You can 1. make each field update with individual actions, changeName, changeAge, etc, or 2. filter the action payload to get rid of the unwanted values before putting them in your obj:
case 'CHANGE_DATA':
return {
...state,
obj: {
...state.obj,
...action.data.filter(el => !!el)
}
}
(the !! notation is converting the array elements to booleans when filtering, not strictly necessary but sanitary)
EDIT: Sorry I misunderstood your data shape, see comments.
Related
I have an array of objects which I display in a grid. The goal is to have multiple filters on the data - one by text in a specific column, one by different date ranges, then sort by each of the columns in ascending/descending order and also have pagination(I already have the helper functions for this in place).
My first approach was to have multiple useEffects inside which I do filter/sort my data and update the state. Problem is that the function that sets the state apparently doesn't update the state right away so in each next useEffect I'm not working with the filtered by the previous one data but with the original data. Basically I need to chain the 3 filter methods, then sorting, then pagination in this order, on the original array, but I don't want to do some of the filtering/sortings unless the user has changed the settings.
Any ideas for the best way to approach this are appreciated. Here's a quick nonworking prototype. https://codesandbox.io/s/peaceful-nigh-jysdy The user can change filters at all times and all 3 should take effect, I also need to set an initial config to hold initial filter values
Your code has 2 main issues:
the reducer is producing new filtered arrays at every dispatch,
you are abusing useEffect and useState and you constructed a very convoluted data flow.
Let me explain both issues.
Reducer filtering
You have n items in the initial state of the reducer.
[
{ id: "124", name: "Michael", dateCreated: "2019-07-07T00:00:00.000Z" },
{ id: "12", name: "Jessica", dateCreated: "2019-08-07T00:00:00.000Z" },
{ id: "53", name: "Olivia", dateCreated: "2019-01-07T00:00:00.000Z" }
]
When you dispatch an action (such as FILTER_BY_TEXT), you are producing a new filtered array:
dispatch({ type: "FILTER_BY_TEXT", payload: { text: 'iv' } });
Gives:
[ { id: "53", name: "Olivia", dateCreated: "2019-01-07T00:00:00.000Z" } ]
But if you then dispatch a new value (in this case ae so that it should match Michael):
dispatch({ type: "FILTER_BY_TEXT", payload: { text: 'ae' } });
You get an empty array!
[]
This is because you are applying the ae filter over an already filtered list!
Convoluted data flow
Your application state is composed of:
the data you want to show, filtered and sorted,
the current values the user has chosen for the filters and sorts.
For every filter/sort “XXX” your current approach uses the following pattern:
function reducer(state, action) {
switch (action.type) {
case 'FILTER_BY_XXX': return filterByXXX(state, action);
default: return state;
}
}
function filterByXXX(state, action) { … }
function App() {
const [state, dispatch] React.useReducer(reducer, []);
// (1) Double state “slots”
const [xxx, setXXX] = React.useState(INITIAL_XXX);
const [inputXXX, setInputXXX] = React.useState(INITIAL_INPUT_XXX);
// (2) Synchronization effects (to apply filters)
React.useEffect(() => {
dispatch({ type: 'FILTER_BY_XXX', payload: xxx });
}, [xxx]);
return (
<input
value={inputXXX}
onChange={event => {
// (4) Store the raw input value
setInputXXX(event.currentTarget.value);
// (5) Store a computed “parsed” input value
setXXX((new Date(event.currentTarget.value));
}}
/>
);
}
Let me show the fallacies:
You don’t need a double state at (1), you are just over-complicating your code.
This is pretty obvious for string values, such as filterByText and inputVal, but for “parsed” values you need a little bit of explanation.
It does make sense to separate the UI driven state from the “parsed” state, but you don’t need to store it in state! In fact you can always re-compute them from the actual input state.
Please have a look at this part of the React documentation: Main Concepts › Thinking in React › Identify The Minimal (but complete) Representation Of UI State
The “correct” approach is to remove the double slots at (1) and remove the setter at (5). You can then recompute the values at render time:
const [inputXXX, setInputXXX] = React.useState(INITIAL_INPUT_XXX);
// Here we just recompute a new Date value from the inputXXX state
const xxx = new Date(inputXXX);
return (
<input
value={inputXXX}
onChange={event => {
setInputXXX(event.currentTarget.value);
}}
/>
);
Your reducer, as explained before, starts with the full data set and is imperatively filtered at every dispatch. Every dispatch instructs the application to add another filter on top of the previously applied filters.
This happens because in the state you only have the result of the previous operations. This is similar to how you can treat long arithmetic operations as a succession of simpler ones:
11 + 23 + 560 + 999 = 1593
can be rewritten as
( ( ( 11 + 23 ) + 560 ) + 999 ) = 1593
( ( ( 34 ) + 560 ) + 999 ) = 1593
( ( 594 ) + 999 ) = 1593
At every step you are losing the information about how you got to that value, but you are seeing the result nonetheless!
When your application wants to “filter by text” it doesn’t want to “add” a filter, but substitute the previous text filter with the new text filter.
You are reacting to changes in state by synchronizing “from the state to the reducer’s state” with an effect.
This is indeed a very correct use of useEffect, but if both the values you are synchronizing from and to are defined in the same place (or very near) as in this case, then probably you are over-complicating your code for no apparent reason.
You can, for example, just dispatch in the event handler:
return <input onChange={event => { dispatch( … ); } />;
But the real problem is the next one.
You are storing computed results as “state”, but it is actually “computed values”. Your real state is the whole dataset, the filtered list is an artifact of “applying filters to the whole dataset”.
You need to free yourself from the fear of computing values at render time:
const [inputVal, setInputVal] = React.useState("");
const [selectTimeVal, setSelectTimeVal] = React.useState("");
const [selectSortVal, setSelectSortVal] = React.useState("");
const data = [ {…}, {…}, {…} ];
let displayData = data;
displayData = filterByText(displayData, inputVal);
displayData = filterByTimeFrame(displayData, selectTimeVal);
displayData = sortByField(displayData, selectSortVal);
return (
<>
<input value={inputVal} onChange={………} />
<select value={selectTimeVal} onChange={………} />
<select value={selectSortVal} onChange={………} />
{displayData.map(data => <p key={data.id}>{data.name}</p>)}
</>
);
Further reading
Once you understand the topics listed above, you may want to avoid useless recalculations, but only once you actually hit some performance issues! (See the very detailed instructions about performance optimizations in the React docs)
If you get to that point, you’ll find very useful the following React APIs and Hooks:
React.PureComponent
React.Component’s shouldComponentUpdate method
React.memo
React.useMemo hook
React.useCallback hook
I have following hierarchy for my app:
tabs
filter
tableData
Now tabs is an array. It contains objects - also the filter object.
Example:
[{type:1, filter:{sortBy:publisDate,asc}, order:0, name: 'fun tab'}];
Now my ideal reducer setup would be:
I have a tab reducer controlling active tab, order, name etc.
I have then a filter tab controlling filter updates.
Best case I do not have to worry about "activeTab" etc - but the reducer handles the logic. So when I update the filter it is reflected correctly in the array of tabs.
I could put all of this in one reducer but it is a lot and will end in mess.
What is a good strategy to do this?
I don't know exactly what do you want to do with this tabs/filters, but if everything will be related in the same screen i don't see bad to do everything in one reducer. for instance, you could have something like this:
TabsReducer
{
activeTab: 'products',
filter: {
term : 'sarasa',
order: 0,
sortBy: 'name asc',
}
}
Then, in your reducer if you want to update your filter or something, you can do
if (type === 'CHANGE_FILTER_TERM') {
return {
...state,
filter: {
...state.filter,
term: payload.term
}
}
}
Given a reducer example like the following
_({
expandAbility: (state, a: { which: string }) => ({
...state,
report: state.report && {
waitingForIt: false,
content: state.report.content && {
...state.report.content,
interaction: {
expandAbilities: !state.report.content.interaction.expandAbilities.contains(a.which)
? state.report.content.interaction.expandAbilities.add(a.which)
: state.report.content.interaction.expandAbilities.remove(a.which)
}
}
}
}),
})
(state type given below just for question context purposes)
const initialState = {
report: undefined as
| {
waitingForIt?: boolean
content?: {
supportedFightIds: number[]
deathCut: number
reportInfo: any
deathsFull: any
deathsByPlayer: any
deathsByAbility: any
interaction: {
expandAbilities: Set<string>
}
}
}
| undefined,
error: undefined as Error | undefined
}
Is there any kind of trick or "flavor-of-the-moment" library which would allow me to write a reducer update operation like expandAbility in a shorter way? (besides maybe creating some vars to reference inner paths)
There are lots of immutable update utilities out there, check out some options at https://github.com/markerikson/redux-ecosystem-links/blob/master/immutable-data.md#immutable-update-utilities and see what would be the best fit for you.
For starters check out Immutability-helper or immer.
So there are two things you could do to help simplify this. The first thing I like to do is move the logic out of the reducer and instead just pass in a value and say set expandAbilities to action. expandAbilities.
The second is actually something we do at work. We use immutableJS and wrote a single reducer that handles all of our state calls because you can give it a path of the parts of state that need to be updated and the value to update it with so we extracted that out and now it is easy to say dispatch(actions.update({path: ['report', 'content', 'interaction', 'expandAbilities'], value: '123' }))
You can even expand this so you can pass in a list of values that need to be updated and even preform validations around the data.
As I understand it, when an action is called, all reducers respond. If action exists in the switch case statement of the reducer, it executes. If it doesn't, then the case: default executes which preserves the existing state.
When the action exists in the reducer but the particular property it's trying to update does not exist, it seems to behave OK as there's nothing to update.
For example, I have an action creator that is used to set the visible property of my modals. Each modal has its own Id. My code looks like this:
export default (state = initialState, action) => {
case types.SET_MODAL_IS_VISIBLE:
return Object.assign({}, state,
{ modal22: action.value }
)}
I have the SET_MODAL_IS_VISIBLE in multiple reducers but if modal22 is not defined in a particular reducer, nothing happens and no errors.
Now, I have a scenario that is throwing an error. I have a general purpose date picker component that I built that can be used as a single and independent date picker OR it can be "linked to" another one. The second scenario is useful if I need the user to give me two dates e.g. start and end dates.
I also built a feature where if the date picker is coupled with another one, when the user sets the date in the first date picker, I disable all the dates prior to that date in the second date picker because I don't want the user to unintentionally select an end date that is prior to the start date.
I define my date pickers as below:
const initialState = {
datePickers: {
"startDatePicker": {
activeDate: "8/25/2017",
disabledBefore: "",
linkedTo: "endDatePicker"
},
"endDatePicker": {
activeDate: "",
disabledBefore: "8/25/2017" // This date is set when the user sets the active date in startDatePicker
linkedTo: ""
}
}
}
This scenario is a bit interesting because a state change in one property in my reducer is triggering a state change in another. This is not difficult to do and I have a way of controlling when I do the update.
The action for setting disabled dates looks like below:
...
case types.SET_DISABLED_DATES:
return Object.assign({}, state,
datePickers: Object.assign({}, state.datePickers, {
datePickers[action.datePickerId]: Object.assign({}, state.datePickers[action.datePickerId], {
disabledBefore: action.value
})
})
Please keep in mind that I can and should be able to set disabledBefore even if the date picker is used as an independent one. So, I need my SET_DISABLED_DATES in every reducer.
The problem I'm running into is that whenever I call SET_DISABLED_DATES, I get errors in reducers where the date picker is used as a single/independent one because the date picker Id for its pair is NOT defined in the reducer.
For example, in projectsReducer I may use the date picker as part of a pair so both startDatePicker and endDatePicker are defined and everything works fine.
But I may be using a single instance date picker in the tasksReducer which also responds to the SET_DISABLED_DATES call but it fails because it cannot find the endDatePicker. In this scenario, the tasksReducer is responding to the call I made to set the disabledDates property of endDatePicker in projectsReducer.
I've posted two questions about this already and the only real solution I'm seeing here is that I need to have a condition in my reducer that looks like this:
...
case types.SET_DISABLED_DATES:
if(typeof state.datePickers[action.datePickerId] !== "undefined") { // Making sure that what I'm trying to update exists in the reducer
return Object.assign({}, state,
datePickers: Object.assign({}, state.datePickers, {
datePickers[action.datePickerId]: Object.assign({}, state.datePickers[action.datePickerId], {
disabledBefore: action.value
})
})
} else {
return state;
}
Admittedly, this looks a bit like a kludge but I couldn't really come up with another solution here.
Again, the problem is that for as long as all reducers respond to SET_DISABLED_DATES, it's guaranteed that a particular date picker will not be there and the Object.assign() will throw an error.
Any suggestions? Is the simple condition in the reducer the way to go here? Is it a kludge?
P.S. I tried this code and it works fine and fixes the problem. On the one hand, I feel this is a bit of an anti-pattern but on the other hand, it just seems like a good idea to make sure the property I want to update in my reducer exists before attempting to update it. I'd appreciate your feedback on this. Thanks.
You are just doing basic validation in the reducer before setting the state. That is perfectly fine. I don't think it will be a good practice to check the store in the action creator to prevent dispatching actions on objects not in the store (how would you do that anyway!).
What I don't understand is, how can a datepicker be linked to another datepicker that isn't in the store? Maybe dispatch a create and teardown action on the component's didMount and willUnmount?
I don't know your full requirements but I think we can make it a lot simpler. I'd do something like this:
The store:
{
datePickers: {
id1: {
value: '',
minValue: '',
maxValue: '',
},
id2: {
value: '',
minValue: '',
maxValue: '',
}
}
}
Now, unless you are making some kind of coupled datepicker components that will always behave in pairs, I believe the cleanest approach would be to set the disabled value in the linked datepicker in the mapDispactchToProps function in your parent component.
That is where you would set ids to the components, and you know exactly which component should be disabled before another.
Something like:
dispatch => ({
setArrivalDate(value) {
dispatch(datePickerActions.setValue(arrivalDateId, value);
dispatch(datePickerActions.setMaxValue(depatureDateId, value);
},
setDepatureDate(value) {
dispatch(datePickerActions.setValue(depatureDateId, value);
dispatch(datePickerActions.setMinValue(arrivalDateId, value);
}
})
This may not be abstract enough, but is clean.
You could do the same thing if you have a paired component, but you'd still need to know which date comes before another. It'd be a hassle to make a generic abstraction around it.
Remove the bold part in your code below
...
case types.SET_DISABLED_DATES:
if(typeof state.datePickers[action.datePickerId] !== "undefined") { // Making sure that what I'm trying to update exists in the reducer
return Object.assign({}, state,
datePickers: Object.assign({}, state.datePickers, {
datePickers[action.datePickerId]: Object.assign({}, state.datePickers[action.datePickerId], {
disabledBefore: action.value
})
})
} else {
return state;
}
Also, a little bit of es6 spread and a helper switchcase function makes this code much more readable.
const newReducer = (state = defaultState, action) => switchcase({
[types.SET_DISABLED_DATES]:
state.datePickers[action.datePickerId] === undefined
? state
: ({ ...state,
datePickers: { ...state.datePickers,
[action.datePickerId]: { ...state.datePickers[action.datePickerId],
disabledBefore: action.value,
},
},
}),
})(state)(action.type);
Using lodash/fp/set, the code becomes
const reducerWithLodash = (state = defaultState, action) =>
switchcase({
[types.SET_DISABLED_DATES]:
state.datePickers[action.datePickerId] === undefined
? state
: set({...state}, `datePickers.${action.datePickerId}.disabledBefore`, action.value)
})(state)(action.type)
I haven't tested the lodash version, so please take that with a grain of salt (Dan Abramov seems to approve)
I'm a bit confused on how I'm supposed to use selectors after I've normalized my Redux store.
I have the following setup for my store:
const DEFAULT_STATE = {
allId: [],
locations: {}
};
With the following for my reducer:
handleActions({
['UPDATE']: (state, action) => {
let newID = state.allId.length;
const allId = [...state.allId, newID];
const locations = {
...state.locations,
[newID]: action.payload
};
return {allId, locations};
}
}),
...
I figured I would want something like this for my component:
function mapStateToProps(state) {
return {
share: callMySelector(state)
};
}
But I don't see how my selector would do anything except return the location associated with the most recent ID. I'm thinking that normalizing is also not that great here - because I wouldn't end up searching by ID in a regular case.
The power of selectors is that it moves filtering logic away from the component consuming the data, and away from the actions/reducer into reusable functions. You mentioned getting the most recent location. From the update logic in the reducer, we'd just make a selector that grabs the last item.
function selectLatestLocation(state) {
const latestId = state.allIds[state.allIds.length - 1];
return state.locations[latestId];
}
This assumes the location data is structured with the location id as the key.
{
1: { id: 1, name: "USA" },
2: { id: 2, name: "Europe" }
}
In this case, normalizing the data isn't doing much. But let's say requirements change, and now we only want Europe locations. We could have another state property called europeIds that contains all Europe location ids.
function selectEuropeLocations(state) {
return state.europeIds.map(id => state.locations[id]);
}
Using selectors with normalized Redux state make it really easy to change how data is filtered. Now like you said, some cases might not need to be normalized. It is really up to the project, and what is being accomplished. But, it's definitely worth it for data that needs to be memoized, or filtered in different ways!