Is there a way to dynamicaly create state with useState in React - javascript

I am building a react app that has input fields that should be stored to the state.
I can easily store these values using the useState hook. The challenge I am facing however is that I do not know the fields as I pull them from a database and they can change. Is there a way that I can set up the useState to be created dynamically so they can be added or removed based on what is in the database. eg.
The fields would be pulled from a database and stored into an array like so:
const fields = [userName, userAge, userLocation, userOccupation];
The useState would be
const[username, setUserName] = useState('');
const[userAge, setUserAge] = useState('');
...
If I added another item to the array, it should automatically create state for that item.
What would be the best way to achieve this?
Thanks.

One way to dynamically create state variables based on the fields array is to use the useReducer hook in combination with a reducer function.
For example;
import { useReducer } from 'react';
const fields = [userName, userAge, userLocation, userOccupation];
const initialState = {};
fields.forEach(field => {
initialState[field] = '';
});
const reducer = (state, action) => {
switch (action.type) {
case 'SET_FIELD_VALUE':
return {
...state,
[action.field]: action.value
};
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, initialState);
// You can then use dispatch to update the state for a specific field
dispatch({ type: 'SET_FIELD_VALUE', field: 'userName', value: 'John' });
This way, you can dynamically create state variables for each field in the fields array, and use the dispatch function to update the state for a specific field.

Related

How do I move data between two different Redux slices?

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
}
}
}

no data is passed into state when using useContext/useReducer together with useQuery

export const itemReducer = (state, action) => {
switch (action.type) {
default:
return state
}
}
import React, { useState, useReducer, createContext, useContext } from 'react'
import { useQuery } from '#apollo/client'
import { CURRENT_MONTH_BY_USER } from '../graphql/queries'
import { itemReducer } from '../reducers/ItemReducer'
const Items = createContext()
export const ItemProvider = ({ children }) => {
let items = []
const [state, dispatch] = useReducer(itemReducer, { items: items })
const result = useQuery(CURRENT_MONTH_BY_USER)
if (result.data && result.data.getCurrentMonthByUser) {
items = [...result.data.getCurrentMonthByUser]
}
return <Items.Provider value={{ state, dispatch }}>{children}</Items.Provider>
}
export const ItemsState = () => {
return useContext(Items)
}
export default ItemProvider
let items gets correct data from the useQuery, however nothing is passed into the state, therefore I am unable to transfer data into another components from the context. What am I doing wrong here?
When debugging both items and state they're initially empty because of the loading however then only the items receives correct data and state remains as empty array.
If i put static data into let items it works just fine, so maybe there can be something wrong with my useQuery as well?
It's easy to see your problem if you look at where items is used. That's only as the initial state to your useReducer call - but items is only set to a non-empty value after this. That has absolutely no effect on the component, because items is not used later in your component function, and the initial state is only ever set once, on the first render.
To solve this you need to embrace your use of a reducer, adding a new action type to set this initial data, and then dispatching that when you have the data. So add something like this to your reducer:
export const itemReducer = (state, action) => {
switch (action.type) {
case SET_INITIAL_DATA: // only a suggestion for the name, and obviously you need to define this as a constant
return { ...state, items: action.items };
/* other actions here */
default:
return state
}
}
and then rewrite your component like this:
export const ItemProvider = ({ children }) => {
const [state, dispatch] = useReducer(itemReducer, { items: [] })
const result = useQuery(CURRENT_MONTH_BY_USER)
if (result.data && result.data.getCurrentMonthByUser) {
dispatch({ type: SET_INITIAL_DATA, items: result.data.getCurrentMonthByUser });
}
return <Items.Provider value={{ state, dispatch }}>{children}</Items.Provider>
}
Also, while this is unrelated to your question, I will note that your ItemsState export appears to be a custom hook (it can't be anything else since it isn't a component but uses a hook) - that is perfectly fine but there is a very strong convention in React that all custom hooks have names of the form useXXX, which I strongly suggest you should follow. So you could rename this something like useItemsState (I would prefer useItemsContext to make clear it's just a useContext hook specialised to your specific context).

set multiple Search Params in the same render cycle

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.

What is the best or most efficient method for removing keys when setting state via React's useState hook?

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 })
}

cant update context state when using hooks with a complex object to set providers value

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)}

Categories