I use a Tree from mui v5 and I want to add in searchParams node that are selected and expanded. To do this I use the hook useSearchParams from React Router (v6).
The fact is that event selected and expanded are firing in the same rendering of the component.
So when the first write params by setSearchParams(...) the second do the same but with the same searchParams and erase params setted by the first.
I made a CodeSandBox which reproduces the behavior.
I try to use a ref to allow to mutate freshSearchParamsbut I did not succeed.
The problem is that the TreeView component is dispatching both onNodeSelect and onNodeToggle on the same Click. One thing you can do is customize both handleToggle and handleSelect functions, so they combine the two expanded and selected variables.
I'd take another approach to this scenario. I'd use a custom hook that handles the Tree state and wraps that state with that searchParams functionality. You can initialize the state from the URL and update the search parameters when the state is updated. I'd implement that URL update with a useEffect that compares status to URL and makes the appropriate updates.
Here's a possible implementation of that custom hook.
const useTreeUrlStatus = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [state, setState] = useState(() => {
return {
selected: searchParams.get("selected") ?? "",
expanded: searchParams.get("expanded")
? searchParams.get("expanded").split(",")
: []
};
});
useEffect(() => {
if (
searchParams.get("selected") !== state.selected ||
searchParams.get("expanded") !== state.expanded
) {
setSearchParams({
selected: state.selected,
expanded: state.expanded.join(",")
});
}
}, [state, searchParams, setSearchParams]);
const updateState = (key, value) => {
setState((prevState) => {
const newState = { ...prevState, [key]: value };
return newState;
});
};
return [state, updateState];
};
You have a working sandbox here forked from your posted sandbox.
Related
I pass a prop data which contain a field worktypeData witch has a field that changes.
When I console log I can see data updated, but the console log in the map is one step behind late.
I tried with a useEffect that set a new state with **data **in the dependencies, but same result.
Tried this too, but same result.
// this is the type of the field in OfficeOccupancyCardData that I want to update
interface WorkTypeData {
selected: WorkType;
onClick: (worktype: WorkType) => void;
buttonLink: string;
}
interface DashboardNextDaysProps {
data: OfficeOccupancyCardData[];
}
const DashboardNextDays: React.FunctionComponent<DashboardNextDaysProps> = ({ data }) => {
// console log here will show data with the new value
return (
<HorizontalGridSlide className="DashboardNextDays" colSize={286} gap={16} pageSlide>
{data.length > 0 &&
data.map((day, i) => {
// console log here will show data with the value not updated
return <OfficeOccupancyCard key={generateKey(i)} data={day} />;
})}
</HorizontalGridSlide>
);
};
EDIT: I found a solution, if someone can explain to me why this works.
In the parent of DashboardNextDays I have a useEffect to set the new data :
useEffect(() => {
setNextDaysData((prev) => {
const newNextDaysData = prev;
if (newNextDaysData && nextDayWorktypeSelected)
newNextDaysData[nextDayWorktypeSelected.dayIndex] = {
...newNextDaysData?.[nextDayWorktypeSelected.dayIndex],
worktypeData: {
...newNextDaysData?.[nextDayWorktypeSelected.dayIndex].worktypeData,
selected: nextDayWorktypeSelected.worktype,
},
};
return newNextDaysData ? newNextDaysData : [];
});
}, [nextDayWorktypeSelected]);
And I just changed
return newNextDaysData ? newNextDaysData : [];
to
return newNextDaysData ? [...newNextDaysData] : [];
To see the state changes immediately, you need to use useEffect and add newData in the dependency change.
Since setState is asynchronous you can not check state updates in console.log synchronously.
useEffect is the hook that manages side-effects in functional components:
useEffect(callback, dependencies). The callback argument is a function to put the side-effect logic in place. Dependencies is a list of your side effect's dependencies, whether they are props or state values.
import { useEffect } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState();
useEffect(() => {
// Side-effect uses `prop` and `state`
}, [prop, state]);
return <div>....</div>;
}
I am using the latest (as of today) version of React+ React-Redux.
When I start my app, I load a list of data I need to store in a slice that is used for one purpose. For this example, a list of table names and their fields.
I also have a slice that manages UI state, Which needs only the table names to create a sub-menu.
The list of tables with all it's data is loaded into slice tree and I need to copy just the table names into a slice called UI.
I am not very clear on the best way (or the right way) to move data between two sibling slices.
There are three ways to reach a state of sibling slice:
getState in dispatch(action)
create an action for UI slice for your purpose, getState returns whole store object, see
export const setUIElements = (payloadOfTheUIDispatch) => (dispatch, getState) => {
const { sliceNameOfTheTable } = getState();
const tableData = sliceNameOfTheTable;
// ... update data as you need:
dispatch(actionThatUpdatesUISlice(data));
}
then read the state that is actionThatUpdatesUISlice updates. Do the reading in your component with useSelector.
use useSelector in the component which creates submenu. Read from both tableSlice and uiSlice:
const tableData = useSelector(state => state.tableData);
const uiData = useSelector(state => state.ui);
// create submenu here
// example:
const subMenu = React.useMemo(() => {
if (!tableData || !uiData) return null;
return tableData.map(item => <div key={item.id}>{item.name}</div>);
},[tableData, uiData]);
If you are performing some kind of expensive calculation to create data for the submenu in ui slice, you can use createSelector, but the first two method is best, if you do not perform any expensive calculation:
export const selectSubMenuItems = createSelector(
state => state.ui,
state => state.table,
(uiData, { stateNameOfTableDataInTableSlice }) => {
const { stateNameFromUISliceThatYouNeed } = uiData;
const expensiveCalculation = differenceBy(
stateNameFromUISliceThatYouNeed,
stateNameOfTableDataInTableSlice.map(item => item.name),
'id',
);
return expensiveCalculation;
},
);
The approach I took eventually was to add a reducer to the second slice that catches the same action as the first slice, hence both get the same payload at the same time. With no dependencies on each other.
But, if I could solve this with a simple selector (like the comment in my questions suggests, it would have been preferred).
first slice:
const SliceA = createSlice({
name: "sliceA",
initialState: {},
reducers: {},
extraReducers(builder) {
builder
.addCase("a1/menueData/loaded", (state,action) => {
//do what I want with action.payload
}
}
}
// in a different file
const SliceB = createSlice({
name: "sliceB",
initialState: {},
reducers: {},
extraReducers(builder) {
builder
.addCase("a1/menueData/loaded", (state,action) => {
//do what I want with action.payload, which is the same one as
//in reducer A above
}
}
}
I have a very simple todo app built with React.
The App.js looks like this
const App = () => {
const [todos, setTodos] = useState(initialState)
const addTodo = (todo) => {
todo.id = id()
todo.done = false
setTodos([...todos, todo])
}
const toggleDone = (id) => {
setTodos(
todos.map((todo) => {
if (todo.id !== id) return todo
return { ...todo, done: !todo.done }
})
)
}
return (
<div className="App">
<NewTodo onSubmit={addTodo} />
<Todos todos={todos} onStatusChange={toggleDone} />
</div>
)
}
export default App
where <NewTodo> is the component that renders the input form to submit new todo item and <Todos /> is the component that renders the list of the todo items.
Now the problem is that when I toggle/change an existing todo item, the <NewTodo> will get re-rendered since the <App /> gets re-rendered and the prop it passes to <NewTodo>, which is addTodo will also change. Since it is a new <App /> every render the function defined in it will also be a new function.
To fix the problem, I first wrapped <NewTodo> in React.memo so it will skip re-renders when the props didn't change. And I wanted to use useCallback to get a memoized addTodo so that <NewTodo> will not get unnecessary re-renders.
const addTodo = useCallback(
(todo) => {
todo.id = id()
todo.done = false
setTodos([…todos, todo])
},
[todos]
)
But I realized that obviously addTodo is dependent upon todos which is the state that holds the existing todo items and it is changing when you toggle/change an existing todo item. So this memoized function will also change.
Then I switched my app from using useState to useReducer, I found that suddenly my addTodo is not dependent upon the state, at least that's what it looks like to me.
const reducer = (state = [], action) => {
if (action.type === TODO_ADD) {
return [...state, action.payload]
}
if (action.type === TODO_COMPLETE) {
return state.map((todo) => {
if (todo.id !== action.payload.id) return todo
return { ...todo, done: !todo.done }
})
}
return state
}
const App = () => {
const [todos, dispatch] = useReducer(reducer, initialState)
const addTodo = useCallback(
(todo) => {
dispatch({
type: TODO_ADD,
payload: {
id: id(),
done: false,
...todo,
},
})
},
[dispatch]
)
const toggleDone = (id) => {
dispatch({
type: TODO_COMPLETE,
payload: {
id,
},
})
}
return (
<div className="App">
<NewTodo onSubmit={addTodo} />
<Todos todos={todos} onStatusChange={toggleDone} />
</div>
)
}
export default App
As you can see here addTodo is only announcing the action that happens to the state as opposed to doing something directly related to the state. So this would work
const addTodo = useCallback(
(todo) => {
dispatch({
type: TODO_ADD,
payload: {
id: id(),
done: false,
...todo,
},
})
},
[dispatch]
)
My question is, does this mean that useCallback never plays nicely with functions that contain useState? Is this ability to use useCallback to memoize the function considered a benefit of switching from useState to useReducer? If I don't want to switch to useReducer, is there a way to use useCallback with useState in this case?
Yes there is.
You need to use the update function syntax of the setTodos
const addTodo = useCallback(
(todo) => {
todo.id = id()
todo.done = false
setTodos((todos) => […todos, todo])
},
[]
)
You've dived down a bit of a rabbit hole! Your original problem was that your addTodo() function depended on the state todos, therefore whenever todos changed you needed to create a new addTodo function and pass that to NewTodo, causing a re-render.
You discovered useReducer, which could help with this since the reducer is passed the current state, and so does not need to capture it in the closure, so it can be stable over changes of todos. However, the React authors have already thought of this situation, and you don't need useReducer (which is really provided as a concession to those who like the Redux style of state updating!). As Gabriele Petrioli pointed out, you can just use the update usage of the state setter. See the docs.
This allows you to write the callback function that Gabriele has provided.
So to answer your final questions:
does this mean that useCallback always does not play nicely with function that contains useState?
useCallback can play perfectly nicely, but you need to be aware of what you are capturing in the closure you pass to useCallback, and if you are using a variable from useState in your callback you need to pass that variable in the list of deps to ensure that your closure is refreshed and it won't be called with out-of-date state.
And then you have to realize that the callback will be a new function thus causing re-renders to components that take it as an argument.
Is this ability to use useCallback to memoize the function considered a benefit of switching from useState to useReducer?
No, not really. useCallback does not prefer useState or useReducer. As I said, useReducer is really there to support a different style of programming, not because it provides functionality that is not available through other means.
If I don't want to switch to useReducer, is there a way to use useCallback with useState in this case?
Yes, as outlined above.
As mentioned by Gabriele Petrioli, You can use the callback syntax or you can keep the value of dependencies of your callback in a ref and use that ref in your callback instead of the state as mentioned here.
In your example, this approach would look something like this:
const [todos, setTodos] = useState([]);
const todosRef = useRef(todos);
useEffect(() => {
todosRef.current = todos;
},[todos]);
const addTodo = useCallback(
todo => {
todo.id = id()
todo.done = false
setTodos([…todosRef.current, todo])
},
[todosRef]
)
Our project is embracing the new functional React components and making heavy use of the various hooks, including useState.
Unlike a React Class's setState() method, the setter returned by useState() fully replaces the state instead of merging.
When the state is a map and I need to remove a key I clone the existing state, delete the key, then set the new state (as shown below)
[errors, setErrors] = useState({})
...
const onChange = (id, validate) => {
const result = validate(val);
if (!result.valid) {
setErrors({
...errors,
[fieldId]: result.message
})
}
else {
const newErrors = {...errors};
delete newErrors[id];
setErrors(newErrors);
}
Is there a better alternative (better being more efficient and/or standard)?
If you need more control when setting a state via hooks, look at the useReducer hook.
This hook behaves like a reducer in redux - a function that receives the current state, and an action, and transforms the current state according to the action to create a new state.
Example (not tested):
const reducer = (state, { type, payload }) => {
switch(type) {
case 'addError':
return { ...state, ...payload };
case 'removeError':
const { [payload.id]: _, ...newState };
return newState;
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, {});
...
const onChange = (id, validate) => {
const result = validate(val);
if (!result.valid) {
dispatch({ type: 'addError', payload: { [id]: result.message }})
}
else {
dispatch({ type: 'removeError', payload: id })
}
When I call toggleFilterSidebar it should toggle the state of filterSidebarIsOpen from false to true and vice versa but onClick nothing happens, but when I pass the Provider value directly as an object it works.
Why does this work?
1).
return <FilterSidebarContext.Provider value={{
toggleFilterSidebar,
filterSidebarIsOpen,
filters,
}}>{children}</FilterSidebarContext.Provider>;
and this doesnt
2).
const [value] = useState({
toggleFilterSidebar,
filterSidebarIsOpen,
filters,
});
return <FilterSidebarContext.Provider value={value}>{children}</FilterSidebarContext.Provider>;
My Code
FilterSidebar.context.js
import React, { useState } from 'react';
export const FilterSidebarContext = React.createContext({});
export const FilterSidebarProvider = ({ children }) => {
const [filterSidebarIsOpen, setFilterSidebarIsOpen] = useState(true);
const toggleFilterSidebar = () => setFilterSidebarIsOpen(!filterSidebarIsOpen);
const [filters] = useState({ regions: [] });
const [value] = useState({
toggleFilterSidebar,
filterSidebarIsOpen,
filters,
});
return <FilterSidebarContext.Provider value={value}>{children}</FilterSidebarContext.Provider>;
};
export const FilterSidebarConsumer = FilterSidebarContext.Consumer;
export default FilterSidebarContext;
FilterButton.js
const FilterButton = ({ className, getTotalActiveFilters }) => {
const { toggleFilterSidebar, filterSidebarIsOpen } = useContext(FilterSidebarContext);
return <Button className={cx({ [active]: filterSidebarIsOpen })} onClick={toggleFilterSidebar} />;
};
With this code:
const [value] = useState({
toggleFilterSidebar,
filterSidebarIsOpen,
filters,
});
you are providing useState with an initial value which is only used when the component is first mounted. It will not be possible for value to ever change since you aren't even creating a variable for the setter (e.g. const [value, setValue] = useState(...)).
I assume you are using useState here to try to avoid a new object being created with each render and thus forcing a re-render of everything dependent on the context even if it didn't change. The appropriate hook to use for this purpose is useMemo:
const value = useMemo(()=>({
toggleFilterSidebar,
filterSidebarIsOpen,
filters
})[filterSidebarIsOpen]);
I've only put filterSidebarIsOpen into the dependencies array, because with your current code it is the only one of the three that can change (toggleFilterSidebar is a state setter which won't change, filters doesn't currently have a setter so it can't change).
useState expects a function to set the value after useState initially does, so if value represents state, setValue would represent setState...
const [value, setValue] = useState(initialValue);
then use setValue to change it
onClick={() => setValue(newValue)}