I recently started using react hooks a lot.
State management seems more intuitive to me when using "React.useState".
Anyway, while it's working ok, I know it's starting to look cluttered the more values I am starting to save to state.
For example, as my car parts app has progressed, it is now looking like this:
const [isShown, setIsShown] = React.useState(false);
const [idVal, setIdValue] = React.useState(false);
const [partNameVal, setPartNameValue] = React.useState(false);
const [engineEngineEngineTypeVal, setEngineEngineTypeValue] = React.useState(false);
const [displacementVal, setDisplacementValue] = React.useState(false);
const [makeVal, setMakeValue] = React.useState(false);
const [countryVal, setCountryValue] = React.useState(false);
const hide = () => setIsShown(false);
const show = (id, partName, engineEngineType, displacement, department, country) => {
setIsShown(true);
setIdValue(id);
setPartNameValue(partName);
setEngineTypeValue(engineEngineType);
setDisplacementValue(displacement);
setMakeValue(department);
setCountryValue(country);
}
<p>ID: {idVal}</p>
<p>PartName: {partNameVal}</p>
<p>EngineType: {engineEngineTypeVal}</p>
<p>Displacement: {displacementVal}</p>
<p>Make: {makeVal}</p>
<p>Country: {countryVal}</p>
I was wondering if there's a way to make this more readable, but still be very intuitive.
Thanks!
Typically you want to handle a single object or use useReducer, something like:
const INITIAL_CAR = {
id: 0,
part: "4xE3",
country: "USA",
// ... More entries
};
const CarApp = () => {
const [car, setCar] = useState(INITIAL_CAR);
const [isShown, setIsShown] = useState(false);
const show = (carProps) => {
setIsShown(true);
setCar(carProps);
};
const { id, part, engine, displacement, make, county } = car;
const updateCountry = (country) =>
setCar((prevCar) => ({ ...prevCar, country }));
const updateCarProperty = ({ property, value }) =>
setCar((prevCar) => ({ ...prevCar, [property]: value }));
return (
<div>
{isShown && (
<>
<p>ID: {id}</p>
<p>PartName: {part}</p>
<p>EngineType: {engine}</p>
<p>Displacement: {displacement}</p>
<p>Make: {make}</p>
<p>Country: {country}</p>{" "}
</>
)}
// use show, updateCountry, updateProperty etc.
</div>
);
};
I'd say that it's the case for useReducer hook.
https://reactjs.org/docs/hooks-reference.html#usereducer
const initialState = {
isShown: false,
idVal: 0,
....
};
function reducer(state, action) {
switch (action.type) {
case 'show':
return {
...state,
isShown: true,
idVal: action.payload.idVal
};
case 'hide':
return {
...state,
isShown: false
}
...
default:
throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({type: 'show', payload: { idVal: 1}})
The way I mostly handle this much of state in a component is using one useState, that way it's just a big object.
Here is a small example :
const [state, setState] = useState({
num: 1,
cars: ['volvo', 'mazda'],
john: {name: 'John', last: 'Foo'}
})
And if you want to change something in that I usually use this function
const onChange = (name, value) => {
setState(prevState => ({...prevState, [name]: value}))
}
This will change the key name to the value value. This is way clearer in my eyes.
You can make a new file to extract all your hook logic from your component.
Call if for example useHooks.js
export default () => {
const [isShown, setIsShown] = React.useState(false);
const [idVal, setIdValue] = React.useState(false);
const [partNameVal, setPartNameValue] = React.useState(false);
const [engineEngineEngineTypeVal, setEngineEngineTypeValue] = React.useState(false);
const [displacementVal, setDisplacementValue] = React.useState(false);
const [makeVal, setMakeValue] = React.useState(false);
const [countryVal, setCountryValue] = React.useState(false);
const hide = () => setIsShown(false);
const show = (id, partName, engineEngineType, displacement, department, country) => {
setIsShown(true);
setIdValue(id);
setPartNameValue(partName);
setEngineTypeValue(engineEngineType);
setDisplacementValue(displacement);
setMakeValue(department);
setCountryValue(country);
}
return [isShown, idVal, partNameVal, engineEngineEngineTypeVal, displacementVal,
makeVal, countryVal, show, hide];
}
The idea was here to put all your hooks logic in a function and return values that you need inside your JSX.
And in your component import and use all properties exported from useHooks
import useHooks from './useHooks';
const [isShown, idVal, partNameVal, engineEngineEngineTypeVal, displacementVal,
makeVal, countryVal, show, hide] = useHooks();
Hope the idea is clear
Related
I did search for those related issues and found some solutions, but most about the lodash debounce. In my case, I create useDebounce as a custom hook and return the value directly.
My current issue is useCallback works with an old debounced value.
Here are my code snips.
//To makes sure that the code is only triggered once per user input and send the request then.
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timeout);
}, [value, delay]);
return debouncedValue;
};
useDebounce works as expected
export const ShopQuantityCounter = ({ id, qty }) => {
const [value, setValue] = useState(qty);
const debounceInput = useDebounce(value, 300);
const dispatch = useDispatch();
const handleOnInputChange = useCallback((e) => {
setValue(e.target.value);
console.info('Inside OnChange: debounceInput', debounceInput);
// dispatch(updateCartItem({ id: id, quantity: debounceInput }));
},[debounceInput]);
console.info('Outside OnChange: debounceInput', debounceInput);
// To fixed issue that useState set method not reflecting change immediately
useEffect(() => {
setValue(qty);
}, [qty]);
return (
<div className="core-cart__quantity">
<input
className="core-cart__quantity--total"
type="number"
step="1"
min="1"
title="Qty"
value={value}
pattern="^[0-9]*[1-9][0-9]*$"
onChange={handleOnInputChange}
/>
</div>
);
};
export default ShopQuantityCounter;
Here are screenshots with console.info to explain what the issue is.
Current quantity
Updated with onChange
I do appreciate it if you have any solution to fix it, and also welcome to put forward any code that needs updates.
This might help you achieve what you want. You can create a reusable debounce function with the callback like below.
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
let timeout;
const setDebounce = (newValue) => {
clearTimeout(timeout);
timeout = setTimeout(() => setDebouncedValue(newValue), delay);
};
return [debouncedValue, setDebounce];
};
And use the function on your code like this.
export const ShopQuantityCounter = ({ id, qty }) => {
const [value, setValue] = useState(qty);
const [debounceInput, setDebounceInput] = useDebounce(value, 300);
const dispatch = useDispatch();
const handleOnInputChange = useCallback((e) => {
setDebounceInput(e.target.value);
console.info('Inside OnChange: debounceInput', debounceInput);
// dispatch(updateCartItem({ id: id, quantity: debounceInput }));
},[debounceInput]);
console.info('Outside OnChange: debounceInput', debounceInput);
// To fixed issue that useState set method not reflecting change immediately
useEffect(() => {
setValue(qty);
}, [qty]);
return (
<div className="core-cart__quantity">
<input
className="core-cart__quantity--total"
type="number"
step="1"
min="1"
title="Qty"
value={value}
pattern="^[0-9]*[1-9][0-9]*$"
onChange={handleOnInputChange}
/>
</div>
);
};
export default ShopQuantityCounter;
Currently, I'm trying to use useState to change a value to an object.
However, the thing I've done doesn't make any errors.
So, I want to know if the way i use UseState is making issues later.
So, my code is like this :
this is where I make useState :
export const SettingContextProvider = props => {
const [title, setTitle] = useState({});
const [label, setLabel] = useState({});
}
and I pass to this component:
const DataSettingPage = () => {
const { layoutValue, chartValue, MainPage } = useContext(CommonStateContext);
const { SetTitle, SetLabel, SetDatas, SetLabels } = useContext(SettingContext);
const [changeLayout] = layoutValue;
const [chart] = chartValue;
const [title, setTitle] = SetTitle;
const [label, setLabel] = SetLabel;
{chart[index].key === "Bar" && (
<BarChart
title={title[id]}
label={label[id]}
/>
)}
and I'm using that state to here as a props :
import React from "react";
import { Bar } from "react-chartjs-2";
const BarChart = ({ title, label }) => {
let data = {
labels: [],
datasets: [
{
label: label,
data: [],
backgroundColor: ["#a7def8e1"],
},
],
};
return (
<>
<Bar
data={data}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: title,
color: "#345fbb",
font: { size: 22 },
},
},
}}
/>
</>
);
};
export default BarChart;
Thank you in advance
You don't need to destructure the passed value further in your child components.
these lines aren't needed:
const [title, setTitle] = SetTitle;
const [label, setLabel] = SetLabel;
Also, I didn't find SetDatas and SetLabels anywhere in your code, have you defined them?
Apart from these two issues, it is perfectly fine to define a state in a parent component and pass both the value and the function to the children:
Parent:
export const SettingContextProvider = props => {
const [title, setTitle] = useState({});
const [label, setLabel] = useState({});
return (<SettingContext.Provider value={[title,setTitle,label,setLabel]}>
<DataSettingPage/>
</SettingContext.Provider>
Child:
const DataSettingPage = () => {
const { layoutValue, chartValue, MainPage } = useContext(CommonStateContext);
const [title,setTitle,label,setLabel] =
useContext(SettingContext); // you can use them as needed
...
Are you calling React.createContext for your context objects? If not I believe your code may have issues as it's not the correct way to use useContext() according to React docs.
Your BarChart component looks like it's missing most of the data (only taking title and one label), so you should fill those in properly.
I would suggest to delete SettingContextProvider completely and simplify the DataSettingPage component as so:
const DataSettingPage = ({title, label}) => {
// I'm not sure what these are for so just leaving them here.
const { layoutValue, chartValue, MainPage } = useContext(CommonStateContext);
const [changeLayout] = layoutValue;
const [chart] = chartValue;
// DELETE
// const { SetTitle, SetLabel, SetDatas, SetLabels } = useContext(SettingContext);
const [title, setTitle] = useState(title)
const [label, setLabel] = useState(label)
{chart[index].key === "Bar" && (
<BarChart
title={title[id]} // <-- Where does your "id" come from??
label={label[id]}
/>
)}
I have a problem with the search engine, it doesn't work properly. When I type some text into input, it searches for something, but when I delete it, all elements disappear. Please help.
State contains names taken from api
const courseReducer = (state, action) => {
switch (action.type) {
case 'SEARCH':
return state.filter(task => task.name.toLocaleLowerCase().includes(action.searchText))
}
}
const [state, dispatch] = useReducer(courseReducer, [])
Form.jsx
const Form = () => {
const { dispatch, state } = useContext(DataContext)
const [text, setText] = useState('')
const handleInput = event => {
setText(event.target.value)
const searchText = text.toLocaleLowerCase();
dispatch({ type: 'SEARCH', searchText })
}
return (
<input type="text" value={text} onChange={handleInput} />
}
Looks like your courseReducer is actually mutating the state after searching. What you want is to do the filter in the Form render itself.
I am currently working on a class-based function. Trying to convert the class to a stateless function, followed by refactoring my code for each event handler from this.SetState to useState (in this case setMovies).
This is my code (partial code):
const Movies = () => {
const [movies, setMovies] = useState(getMovies());
const [disabled, setDisabled] = useState(true);
const [pageSize, setPageSize] = useState(4);
const sortBy = sortType => {
setMovies(movies.filter(sortType));
setDisabled(false);
// this.setState({
// movies: this.state.movies.sort(sortType),
// isDisabled: false,
// });
};
it seems that It is not possible to filter this state. I am able to change to boolean but can't filter my Array. Is there a way to filter using Hooks?
Thanks in advance.
Nothing changes...
const List = () => {
const [items, setItems] = useState([1, 2, 3, 4, 5, 6])
const filterEvenResults = () => setItems(items => items.filter(x => x % 2))
return (
<div>
{
items.map(item => <p key={item}>{item}</p>)
}
<button onClick={filterEvenResults}>Filter</button>
</div>
)
}
Since React hooks rely on the execution order one should generally not use hooks inside of loops. I ran into a couple of situations where I have a constant input to the hook and thus there should be no problem. The only thing I'm wondering about is how to enforce the input to be constant.
Following is a simplified example:
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = constantIdArray.map((id) => useCallback(() => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
}));
return { state, callbacks };
}
const idArray = ['id-1', 'id-2', 'id-3'];
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput(idArray);
return (
<div>
<div onClick={callbacks[0]}>
{state[0]}
</div>
<div onClick={callbacks[1]}>
{state[1]}
</div>
<div onClick={callbacks[2]}>
{state[2]}
</div>
</div>
)
}
Is there a pattern for how to enforce the constantIdArray not to change? My idea would be to use a creator function for the hook like this:
const createUseHookWithConstantInput = (constantIdArray) => () => {
...
}
const idArray = ['id-1', 'id-2', 'id-3'];
const useHookWithConstantInput = createUseHookWithConstantInput(idArray)
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput();
return (
...
)
}
How do you solve situations like this?
One way to do this is to use useEffect with an empty dependency list so it will only run once. Inside this you could set your callbacks and afterwards they will never change because the useEffect will not run again. That would look like the following:
const useHookWithConstantInput = (constantIdArray) => {
const [state, changeState] = useState({});
const [callbacks, setCallbacks] = useState([]);
useEffect(() => {
changeState(buildInitialState(constantIdArray));
const callbacksArray = constantIdArray.map((id) => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
});
setCallbacks(callbacksArray);
}, []);
return { state, callbacks };
}
Although this will set two states the first time it runs instead of giving them initial values, I would argue it's better than building the state and creating new callbacks everytime the hook is run.
If you don't like this route, you could alternatively just create a state like so const [constArray, setConstArray] = useState(constantIdArray); and because the parameter given to useState is only used as a default value, it'll never change even if constantIdArray changes. Then you'll just have to use constArray in the rest of the hook to make sure it'll always only be the initial value.
Another solution to go for would be with useMemo. This is what I ended up implementing.
const createCallback = (id, changeState) => () => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
};
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = useMemo(() =>
constantIdArray.map((id) => createCallback(id, changeState)),
[],
);
return { state, callbacks };
};