How can I make my React component dynamic? - reactjs

At the moment, there is a component that is essentially copied 4 times. I would like to make it more abstract, and simply render it 4 times, and pass in the dynamic data each time. The data that's passed into each component are state hooks.
With that being the goal, could I get some help on the implementation?
Here's what a component call looks like in the parent:
const [allBlueItems, setAllBlueItems] = useState([]);
const [selectedBlueItems, setSelectedBlueItems] = useState([]);
const [allRedItems, setAllRedItems] = useState([]);
const [selectedRedItems, setSelectedRedItems] = useState([]);
<BlueSelection
allBlueItems={allBlueItems}
selectedBlueItems={setSelectedBlueItems}
setSelectedBlueItems={selectedBlueItems}
/>
<RedSelection
allRedItems={allRedItems}
selectedRedItems={setSelectedRedItems}
setSelectedRedItems={selectedRedItems}
/>
Then, the ItemSelection component uses those useState values passed in as props to render data, and updates the state accordingly:
const BlueSelection = ({ allBlueItems, selectedBlueItems, setSelectedBlueItems }) => {
useEffect(() => {
setSelectedBlueItems([]);
}
}, []);
Then I repeat myself and implement the exact same code to handle the RedItem
const RedSelection = ({ allRedItems, selectedRedItems, setSelectedRedItems }) => {
useEffect(() => {
setSelectedRedItems([]);
}
}, []);

Refactor
export default const Selection = () => {
const [allItems, setAllItems] = useState([]);
const [selectedItems, setSelectedItems] = useState([]);
return (
<Selection
allItems={allItems}
selectedItems={setSelectedItems}
setSelectedItems={selectedItems}
/>)}
import this wherever you need it like....
import Selection as RedSelection from ...
import Selection as BlueSelection from...

Related

React : how to prevent the hidden component from appearing during rendering?

During rendering of the part below, the component CreateGarden appears for 1 sec even if the condition is true.
{userInfo.gardenName ?
<ShowPlants />
:
<CreateGarden setModal={setModal} />
}
Here is the custom hook I use to get userInfo
export default function useUserInfo() {
const [userInfo, setUserInfo] = useState([]);
const [categories, setCategories]= useState([]);
const userRef = doc(allUsers,auth.currentUser.uid);
useEffect(() => {
const unsub = onSnapshot(userRef, (doc)=>{
setUserInfo(doc.data())
setCategories(doc.data().categories)
})
return ()=>unsub()
},[auth.currentUser]);
return {userInfo, categories};
}
I tried creating loading state in the hook and use it as additional condition to render but it's still the same , I keep having a glimpse of the hidden component.
EDIT//SOLUTION:
Finally, after different versions I used the following solution
export default function useUserInfo() {
const [userInfo, setUserInfo] = useState({});
const [loading, setLoading ] = useState(true);
const [categories, setCategories]= useState([]);
const userRef = doc(allUsers,auth.currentUser.uid);
useEffect(() => {
const unsub = onSnapshot(userRef, (doc)=>{
setUserInfo(doc.data())
setCategories(doc.data().categories)
})
setLoading(false)
return ()=>unsub()
},[auth.currentUser]);
return {userInfo, categories, loading};
}
In the component:
{userInfo.gardenName && <ShowPlants />}
{!userInfo.gardenName && !loading && <CreateGarden setModal={setModal} />}
You could display a loading icon while the data is being fetched.
When
data hasn't been fetched yet (undefined) -> show loading icon (your hook could return a loading value as well as an error value in case something went wrong)
data has been fetched and there is a gardenName, display ShowPlants
data has been fetched and there is no gardenName, display CreateGarden
What if you use them separately?
{ userInfo.gardenName && <ShowPlants /> }
{ !userInfo.gardenName && <CreateGarden setModal={setModal} /> }

React getting previous version of state when calling a function in useContext

I am using a context like the following:
const placeCurrentOrder = async () => {
alert(`placing order for ${mealQuantity} and ${drinkQuantity}`)
}
<OrderContext.Provider
value={{
placeCurrentOrder,
setMealQuantity,
setDrinkQuantity,
}}
>
and I'm calling this context deep down with something like this (when the user clicks a button):
const x = () => {
orderContext.setMealQuantity(newMealQuantity)
orderContext.setDrinkQuantity(newDrinkQuantity)
await orderContext.placeCurrentOrder()
}
Sort of like I expect, the state doesn't update in time, and I always get the previous value of the state. I don't want to have a useEffect, because I want control over exactly when I call it (for example, if mealQuantity and drinkQuantity both get new values here, I don't want it being called twice. The real function is far more complex.)
What is the best way to resolve this? I run into issues like this all the time but I haven't really gotten a satisfactory answer yet.
You can set them in a ref. Then use the current value when you want to use it. The easiest way is probably to just create a custom hook something like:
const useStateWithRef = (initialValue) => {
const ref = useRef(initialValue)
const [state, setState] = useState(initialValue)
const updateState = (newState) => {
ref.current = typeof newState === 'function' ? newState(state) : newState
setState(ref.current)
}
return [state, updateState, ref]
}
then in your context provider component you can use it like:
const [mealQuantity, setMealQuantity, mealQuantityRef] = useStateWithRef(0)
const [drinkQuantity, setDrinkQuantity, drinkQuantityRef] = useStateWithRef(0)
const placeOrder = () => {
console.log(mealQuantityRef.current, drinkQuantityRef.current)
}
You can also just add a ref specifically for the order and then just update it with a useEffect hook when a value changes.
const [drinkQuantity, setDrinkQuantity] = useState(0)
const [mealQuantity, setMealQuantity] = useState(0)
const orderRef = useRef({
drinkQuantity,
mealQuantity
})
useEffect(() => {
orderRef.current = {
drinkQuantity,
mealQuantity,
}
}, [drinkQuantity, mealQuantity])
const placeOrder = () => {
console.log(orderRef.current)
}

Problem with the apexcharts-react and useEffect

I do the weather app and need some help. In component Chart in options and series comes [object Object]. When you change something in the code, it is displayed. I think that the problem with useEffect? but I don't know how to fix that
import React, { useContext, useState, useEffect } from 'react';
import Chart from 'react-apexcharts';
import { Context } from '../../contex';
const WeatherGrapth = () => {
const {dailyForecast} = useContext(Context);
const [category, setCategory] = useState([])
const [data, setData] = useState([])
useEffect(() => {
const day = [];
const temp =[];
const items = dailyForecast.map((d) => {
const unixTimestamp = d.dt;
const getTemp = Math.round(d.temp.day)
let getDay = new Date(unixTimestamp* 3600 * 24 * 1000).getDate();
day.push(getDay)
temp.push(getTemp)
})
setCategory(day)
setData(temp)
}, []);
return(
<div>
<Chart options={{
chart: {
id: 'weather-graph'
},
xaxis: {
categories: category,
title: {
text: 'Date',
},
},
yaxis: {
title: {
text: 'Temperature °C',
},
},
}}
series={[{
name: 'temp',
data: data
}]} type="line" height={'349px'} />
</div>
)
}
export default WeatherGrapth;
But as soon as I change something in the code, everything will update and a graph will appear.
As React doc says:
By default, effect runs both after the first render and after every update
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders.
Probably At first dailyForecast context is empty or has not any valid data and after that it fills with data you should pass it to useEffect as dependency to run the effect at changes:
const {dailyForecast} = useContext(Context);
const [category, setCategory] = useState([])
const [data, setData] = useState([])
useEffect(() => {
const day = [];
const temp =[];
const items = dailyForecast.map((d) => {
const unixTimestamp = d.dt;
const getTemp = Math.round(d.temp.day)
let getDay = new Date(unixTimestamp* 3600 * 24 * 1000).getDate();
day.push(getDay)
temp.push(getTemp)
})
setCategory(day)
setData(temp)
}, [dailyForecast]);

How to use the same variable in two useStates?

import {useState} from 'react'
const CartItem = (props) => {
const [quantity, setAddQuantity] = useState(1);
const [, setSubQuantity] = useState(0)
const addquantityHandler = (event) => {
setAddQuantity(quantity+1)
}
const subquantityHandler = (event) => {
setSubQuantity(quantity-1)
}
return (
<div>
<div>CartItem</div>
<div>{props.title}</div>
<button onClick={addquantityHandler}>Add One</button>
<button onClick={subquantityHandler}>Subtract One</button>}
<p>Quantity : {quantity}</p>
</div>
)
}
export default CartItem;
I am starting to learn React. Is there any way to subtract quantity using setSubQuantity. Is this impossible to do as quantity was declared in a different useState?
You can achieve this using only one useState.
const [quantity, setQuantity] = useState(1)
const addquantityHandler = (event) => {
setQuantity(quantity+1)
}
const subquantityHandler = (event) => {
setQuantity(quantity-1)
}
How to use the same variable in two useStates?
You can't, and you shouldn't. If you want to track the state of a thing then put that thing in one state variable.
Instead have each of your click handler functions call setAddQuantity with the new value (and rename setAddQuantity to setQuantity.

React hook with constant input parameter - hook creator?

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

Resources