React: Mutate nested states once - reactjs

So in typical programming scenarios, if you mutate an object, it mutates the object everywhere. However, in React, since states are immutable and only mutable through each's set*, if states are nested in each other, like the scenario shown below, only the currentMember's name will change, and not the version of currentMember in currentTeam, or teams. I'd have to go through each and mutate them one by one, which is cumbersome.
What would be the best way to mutate multiple states at once, or achieve a similar effect? I've done so by indexing, but it's more cumbersome to work with, and i didnt know if there were a textbook hook that fixes this.
import React, { useState } from 'react'
interface Member {
name: string
}
interface Team {
name: string
members: Member[]
}
export default (props: {}) => {
const [teams, setTeams] = useState<Team[]>([
{
name: 'Team One',
members: [{ name: 'Wyatt' }, { name: 'Michael' }]
}
])
const [currentTeam, setCurrentTeam] = useState<Team>(teams[0])
const [currentMember, setCurrentMember] = useState<Member>(currentTeam.members[0])
return (
<>
<h1>${currentMember.name}</h1>
<button onClick={() => setCurrentMember(currentMember => { ...currentMember, name: 'Zach' })}>
Change current member name to Zach!
</button>
</>
)
}

As mentioned, you are making things a bit complicated by using state this way. You should have one base state that contains all of the teams, then reference the bits that are important to you by index.
For example, your teams state is fine as is. Your currentTeam and currentMember states should be indexes or some other reference to the state within teams you want to map to.
So, in specific terms, I'd change the format of your code here like so (forgive me as I don't write TypeScript, so I'm going to straight vanilla javascript to avoid making typos):
import React, { useState } from 'react'
// interface Member {
// name: string
//}
// interface Team {
// name: string
// members: Member[]
//}
export default (props: {}) => {
const [teams, setTeams] = useState([
{
name: 'Team One',
members: [{ name: 'Wyatt' }, { name: 'Michael' }]
}
])
const [currentTeamIndex, setCurrentTeam] = useState(0)
const [currentMemberIndex, setCurrentMember] = useState(0)
return (
<>
<h1>${teams[currentTeamIndex].members[currentMemberIndex]}</h1>
<button onClick={() => setTeams(teams => ({
// Shallow copy the teams via mapping through them
...teams.map((team, teamIndex) => {
// If the current team index isn't the index we're on right now, then just
// return the existing team in its given place.
if (teamIndex !== currentTeamIndex) return team
// If we're at this point, it means the teamIndex matches the currentTeamIndex
// and we need to mutate this. We'll just do that by returning a new object
return {
...team, // Make sure we don't miss anything
...members.map((member, memberIndex) => {
// Same as the outer map, if the current member index isn't the same as the
// given memberIndex, then just return the member we're on, we're not mutating it
if (memberIndex !== currentMemberIndex) return member
return {
...member,
name: 'Zach'
}
})
}
}
})
>
Change current member name to Zach!
</button>
</>
)
}
As you can see, drilling that far down into an object isn't so simple, but it's possible to do without mutating the original data - you can accomplish what you are looking for by reconstructing the data on the fly with Array functions, spread syntax, and index references in state.
You might also want to consider confining this in a reducer, with a specific action containing the team id index, and member id index to make this quite a bit more composable.

Related

Is this the correct way to update a propery in objects array state

I've got the code below and i wanna update name property in the object that has id 1. I'm updating with the code objArray[1].name = "Xxx". It perfectly works but is this correct? Should i use prevState with setObjArray. That looked so much easier what you think?
const [objArray, setObjArray] = useState([
{
id:1,
name:"Eren"
},
{
id:2,
name:"Eren2"
},
{
id:3,
name:"Eren3"
}
])
No this is not advisable. You have the useState second array element (setObjArray) for updating state. Read documentation for React useState . There are two basic ways but there isn't much difference. First method;
const changeName = (id, newName) => {
// map through the array and change the name of the element with the id you are targeting
const changedArr = objArray.map((element) => {
if (element.id === id) {
return {
...element,
name: newName,
};
} else {
return element;
}
});
// set the returned array as you new state
setObjArray(changedArr)
};
Second method;
You have access to the previous state. This way you can make changes on the previous state and return the new array as your new state.
const newChangeName = (id, newName) => {
setObjArray((prev) => {
// map through the array and change the name of the element with the id you are targeting
// set the returned array as you new state
return prev.map((element) => {
if (element.id === id) {
return {
...element,
name: newName,
};
} else {
return element;
}
});
});
};
Hope this helped.
There are many ways to do this. Let me share one way to do this:
Make a shallow copy of the array
let temp_state = [...objArray];
Make a shallow copy of the element you want to mutate
let temp_element = { ...temp_state[0] };
Update the property you're interested in
temp_element.name = "new name";
Put it back into our array. N.B. we are mutating the array here, but that's why we made a copy first
temp_state[0] = temp_element;
Set the state to our new copy
setObjArray(temp_state);

How can I make transient/one-off state changes to a React component from one of its ancestors?

I want to modify the state of a child component in React from a parent component a couple levels above it.
The child component is a react-table with pagination.
My use case is changing the data in the table with some client-side JS filtering.
The problem is, the table uses internal state to keep track of which page is being shown, and does not fully update in response to my filtering.
It is smart enough to know how much data it contains, but not smart enough to update the page it is on.
So, it might correctly say "Showing items 21-30 of 85", and then the user filters the data down to only four total items, and the table will say "Showing items 21-30 of 4".
I tried implementing something like what the FAQ suggests for manual state control, but that caused its own problem.
I was passing the new page index in as a prop, and that did set the page correctly, but it broke the ability for the user to navigate between pages, because any changes were immediately overwritten by the value of the prop.
Those instructions seem to work for a situation where all page index control gets handled by the parent, but not when some control should still be retained by the pagination mechanism.
I think what I need is an exposed function that lets me modify the value of the table's state.pageIndex as a one-off instead of passing a permanent prop. Is there a way to do that? Or any other way to solve my underlying problem?
Code follows. I apologize in advance I couldn't make this a real SSCCE, it was just too complicated, I tried to at least follow the spirit of SSCCEs as much as I could.
My page that lists stuff for the user looks like this:
// ...
const [searchTerms, setSearchTerms] = useState<Array<string>>([]);
// ...
const handleFilterRequestFromUser = function (searchTerms): void {
// ...
setSearchTerms(processedSearchTerms);
};
// ...
const visibleData = useMemo(() => {
// ...
}, [searchTerms]);
// ...
return (
<div>
// ...
<ImmediateParentOfTable
id={"Results"}
visibleData={visibleData} // User actions can affect the size of this
// ...
>
// ...
</div>
);
export default ListDatabaseResults;
Here's ImmediateParentOfTable:
import { Table, Pagination } from "#my-company/react";
// ...
return (
<Table
id={id}
pagination={{
render: (
dataSize,
{
pageCount,
pageOptions,
// ...
}
) => (
<Pagination
dataSize={dataSize}
pageCount={pageCount}
pageOptions={pageOptions}
gotoPage={gotoPage}
previousPage={previousPage}
nextPage={nextPage}
setPageSize={setPageSize}
canPreviousPage={canPreviousPage}
canNextPage={canNextPage}
pageIndex={pageIndex}
pageSize={pageSize}
pageSizeOptions={[10, 20, 50, 100]}
/>
),
manual: {
onPageChange: ({
pageIndex,
pageSize,
}: {
pageIndex: number;
pageSize: number;
}) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
},
rowCount,
pageCount: tablePageCount,
},
isLoading: !!dataLoading,
}}
/>
);
The custom Table inside #my-company/react (already in use in other places, so, difficult to modify):
import {
CellProps,
Column,
Hooks,
Row,
SortingRule,
TableState,
useFlexLayout,
usePagination,
UsePaginationInstanceProps,
UsePaginationState,
useRowSelect,
useSortBy,
useTable,
} from 'react-table';
// ...
export interface TableProps<D extends Record<string, unknown>> {
id: string;
// ...
pagination?: Pagination<D>;
pageIndexOverride?: number; // This is the new prop I added that breaks pagination
}
const Table = <D extends Record<string, unknown>>({
id,
columns,
data,
// ...
pageIndexOverride,
}: TableProps<D>): JSX.Element => {
const {
state: { pageIndex, pageSize, sortBy },
// ...
} = useTable(
{
columns,
data,
autoResetPage,
initialState,
useControlledState: (state) => {
return React.useMemo(
() => ({
...state,
pageIndex: pageIndexOverride || state.pageIndex, // This always resets page index to the prop value, so changes from the pagination bar no longer work
}),
[state],
);
},
// ...
I've encountered a similar problem with react-table where most of my functionality (pagination, sorting, filtering) is done server-side and of course when a filter is changed I must set the pageIndex back to 0 to rectify the same problem you have mentioned.
Unfortunately, as you have discovered, controlled state in v7 of react-table is both poorly documented and apparently just completely non-functional.
I will note that the example code you linked from the docs
const [controlledPageIndex, setControlledPage] = React.useState(0)
useTable({
useControlledState: state => {
return React.useMemo(
() => ({
...state,
pageIndex: controlledPageIndex,
}),
[state, controlledPageIndex]
)
},
})
is actually invalid. controlledPageIndex cannot be used as a dep in that useMemo because it is in the outer scope and is accessed through closure. Mutating it will do nothing, which is actually noted by eslint react/exhaustive-deps rule so it's quite surprising that this made it into the docs as a way of accomplishing things. There are more reasons why it is unusable, but the point is that you can forget using useControlledState for anything.
My suggestion is to use the stateReducer table option and dispatch a custom action that will do what you need it to. The table reducer actions can have arbitrary payloads so you can do pretty much whatever you want. ajkl2533 in the github issues used this approach for row selection (https://github.com/TanStack/react-table/issues/3142#issuecomment-822482864)
const reducer = (newState, action) => {
if (action.type === 'deselectAllRows') {
return { ...newState, selectedRowIds: {} };
}
return newState;
}
...
const { dispatch, ... } = useTable({ stateReducer: reducer }, ...);
const handleDeselectAll = () => {
dispatch({ type: 'deselectAllRows' });
}
It will require getting access to the dispatch from the useTable hook though.

React Hooks - keep arguments reference in state

I created a hook to use a confirm dialog, this hook provides the properties to the component to use them like this:
const { setIsDialogOpen, dialogProps } = useConfirmDialog({
title: "Are you sure you want to delete this group?",
text: "This process is not reversible.",
buttons: {
confirm: {
onPress: onDeleteGroup,
},
},
width: "360px",
});
<ConfirmDialog {...dialogProps} />
This works fine, but also I want to give the option to change these properties whenever is needed without declaring extra states in the component where is used and in order to achieve this what I did was to save these properties in a state inside the hook and this way provide another function to change them if needed before showing the dialog:
interface IState {
isDialogOpen: boolean;
dialogProps: TDialogProps;
}
export const useConfirmDialog = (props?: TDialogProps) => {
const [state, setState] = useState<IState>({
isDialogOpen: false,
dialogProps: {
...props,
},
});
const setIsDialogOpen = (isOpen = true) => {
setState((prevState) => ({
...prevState,
isDialogOpen: isOpen,
}));
};
// Change dialog props optionally before showing it
const showConfirmDialog = (dialogProps?: TDialogProps) => {
if (dialogProps) {
const updatedProps = { ...state.dialogProps, ...dialogProps };
setState((prevState) => ({
...prevState,
dialogProps: updatedProps,
}));
}
setIsDialogOpen(true);
};
return {
setIsDialogOpen,
showConfirmDialog,
dialogProps: {
isOpen: state.isDialogOpen,
onClose: () => setIsDialogOpen(false),
...state.dialogProps,
},
};
};
But the problem here is the following:
Arguments are passed by reference so if I pass a function to the button (i.e onDeleteGroup) i will keep the function updated to its latest state to perform the correct deletion if a group id changes inside of it.
But as I'm saving the properties inside a state the reference is lost and now I only have the function with the state which it was declared at the beginning.
I tried to add an useEffect to update the hook state when arguments change but this is causing an infinite re render:
useEffect(() => {
setState((prevState) => ({
...prevState,
dialogProps: props || {},
}));
}, [props]);
I know I can call showConfirmDialog and pass the function to update the state with the latest function state but I'm looking for a way to just call the hook, declare the props and not touch the dialog props if isn't needed.
Any answer is welcome, thank you for reading.
You should really consider not doing this, this is not a good coding pattern, this unnecessarily complicates your hook and can cause hard to debug problems. Also this goes against the "single source of truth" principle. I mean a situation like the following
const Component = ({title}: {title?: string}) => {
const {showConfirmDialog} = useConfirmDialog({
title,
// ...
})
useEffect(() => {
// Here you expect the title to be "title"
if(something) showConfirmDialog()
}, [])
useEffect(() => {
// Here you expect the title to be "Foo bar?"
if(somethingElse) showConfirmDialog({title: 'Foo bar?'})
}, [])
// But if the second dialog is opened, then the first, the title will be
// "Foo bar?" in both cases
}
So please think twice before implementing this, sometimes it's better to write a little more code but it will save you a lot debugging.
As for the answer, I would store the props in a ref and update them on every render somehow like this
/** Assign properties from obj2 to obj1 that are not already equal */
const assignChanged = <T extends Record<string, unknown>>(obj1: T, obj2: Partial<T>, deleteExcess = true): T => {
if(obj1 === obj2) return obj1
const result = {...obj1}
Object.keys(obj2).forEach(key => {
if(obj1[key] !== obj2[key]) {
result[key] = obj2[key]
}
})
if(deleteExcess) {
// Remove properties that are not present on obj2 but present on obj1
Object.keys(obj1).forEach(key => {
if(!obj2.hasOwnProperty(key)) delete result[key]
})
}
return result
}
const useConfirmDialog = (props) => {
const localProps = useRef(props)
localProps.current = assignChanged(localProps.current, props)
const showConfirmDialog = (changedProps?: Partial<TDialogProps>) => {
localProps.current = assignChanged(localProps.current, changedProps, false)
// ...
}
// ...
}
This is in case you have some optional properties in TDialogProps and you want to accept Partial properties in showConfirmDialog. If this is not the case, you could simplify the logic a little by removing this deleteExcess part.
You see that it greatly complicates your code, and adds a performance overhead (although it's insignificant, considering you only have 4-5 fields in your dialog props), so I really recommend against doing this and just letting the caller of useConfirmDialog have its own state that it can change. Or maybe you could remove props from useConfirmDialog in the first place and force the user to always pass them to showConfirmDialog, although in this case this hook becomes kinda useless. Maybe you don't need this hook at all, if it only contains the logic that you have actually shown in the answer? It seems like pretty much the only thing it does is setting isDialogOpen to true/false. Whatever, it's your choice, but I think it's not the best idea

React and Redux Architecture

I have questions about React and Redux architecture.
Suppose you have normalized data
const user = [
{ userId: 1, name: 'Ian', groupId: 1 },
{ userId: 2, name: 'Tom', groupId: 2 }
]
const groups = [
{ groupId: 1, groupName: 'Facebook developers'},
{ groupId: 2, groupName: 'Google developers'}
]
const chat = {
{ userIdFrom: 1, message: 'Hello!', timestamp: 1324132332 },
}
What is the best practice to manipulate data such as .map, .filter, .reduce, .forEach, .sort, denormalization etc?
I create utils functions to handle huge data manipulation like Utils.getChatsFromUsers etc and invoked from render method on Component.
Is this my solution good practice to handle huge and many data manipulation in Component's render function?
Please give me your advise and insight.
Thanks!
It is not really a good practice to manipulate arrays inside the render method unless the array is the only thing being rendered, or if the array is the prop the will most likely change. You should manipulate them in the componentWillReceiveProps.
So, instead of having something like this:
render() {
const { string1, string2, string3, array } = this.props;
const mappedArray = array.map(element => <li>{element}</li>);
return (
<div>
<p>{string1}</p>
<p>{string3}</p>
<p>{string3}</p>
<ul>{mappedArray}</ul>
</div>
);
}
you should do something like this:
componentWillReceiveProps(nextProps) {
// R is from ramda, but you can compare the arrays any way you want
// even just comparing the array references if that's good enough for you
if (R.equals(nextProps.array, this.props.array)) {
this.mappedArray = array.map(element => <li>{element}</li>);
}
}
render() {
const { string1, string2, string3, array } = this.props;
return (
<div>
<p>{string1}</p>
<p>{string3}</p>
<p>{string3}</p>
<ul>{this.mappedArray}</ul>
</div>
);
}
This way you avoid having to recreate the mapped array every time the render method is called (which could happen from other props changing or the state being changed).
This topic is covered here.

React Redux, how to properly handle changing object in array?

I have a React Redux app which gets data from my server and displays that data.
I am displaying the data in my parent container with something like:
render(){
var dataList = this.props.data.map( (data)=> <CustomComponent key={data.id}> data.name </CustomComponent>)
return (
<div>
{dataList}
</div>
)
}
When I interact with my app, sometimes, I need to update a specific CustomComponent.
Since each CustomComponent has an id I send that to my server with some data about what the user chose. (ie it's a form)
The server responds with the updated object for that id.
And in my redux module, I iterate through my current data state and find the object whose id's
export function receiveNewData(id){
return (dispatch, getState) => {
const currentData = getState().data
for (var i=0; i < currentData.length; i++){
if (currentData[i] === id) {
const updatedDataObject = Object.assign({},currentData[i], {newParam:"blahBlah"})
allUpdatedData = [
...currentData.slice(0,i),
updatedDataObject,
...currentData.slice(i+1)
]
dispatch(updateData(allUpdatedData))
break
}
}
}
}
const updateData = createAction("UPDATE_DATA")
createAction comes from redux-actions which basically creates an object of {type, payload}. (It standardizes action creators)
Anyways, from this example you can see that each time I have a change I constantly iterate through my entire array to identify which object is changing.
This seems inefficient to me considering I already have the id of that object.
I'm wondering if there is a better way to handle this for React / Redux? Any suggestions?
Your action creator is doing too much. It's taking on work that belongs in the reducer. All your action creator need do is announce what to change, not how to change it. e.g.
export function updateData(id, data) {
return {
type: 'UPDATE_DATA',
id: id,
data: data
};
}
Now move all that logic into the reducer. e.g.
case 'UPDATE_DATA':
const index = state.items.findIndex((item) => item.id === action.id);
return Object.assign({}, state, {
items: [
...state.items.slice(0, index),
Object.assign({}, state.items[index], action.data),
...state.items.slice(index + 1)
]
});
If you're worried about the O(n) call of Array#findIndex, then consider re-indexing your data with normalizr (or something similar). However only do this if you're experiencing performance problems; it shouldn't be necessary with small data sets.
Why not using an object indexed by id? You'll then only have to access the property of your object using it.
const data = { 1: { id: 1, name: 'one' }, 2: { id: 2, name: 'two' } }
Then your render will look like this:
render () {
return (
<div>
{Object.keys(this.props.data).forEach(key => {
const data = this.props.data[key]
return <CustomComponent key={data.id}>{data.name}</CustomComponent>
})}
</div>
)
}
And your receive data action, I updated a bit:
export function receiveNewData (id) {
return (dispatch, getState) => {
const currentData = getState().data
dispatch(updateData({
...currentData,
[id]: {
...currentData[id],
{ newParam: 'blahBlah' }
}
}))
}
}
Though I agree with David that a lot of the action logic should be moved to your reducer handler.

Resources