React.useCallback not updating its implementation when state is updated - javascript

I am creating a todo application using React hooks. The way I am doing this is by creating a context for a single todo and a context for all todos. The NewTodoContext would be parent to the new todo react components and will take up responsibilities like having an internal state, providing callback to update that state, reset that state and update AllTodosContext once user confirms todo formation.
The NewTodoProvider would look like
function NewTodoProvider({ children }){
const [todo, setTodo] = React.useState(DEFAULT_EMPTY_TODO_STATE);
const [ addTodo ] = React.useContext(AllTodosContext);
const updateTodo = React.useCallback((todoFragment) => {
// setTodo({...todo, ...todoFragment})
}, [todo]);
const confirmTodo = React.useCallback(() => { addTodo(todo) }, [todo]);
return (
<NewTodoContext.Provider value={{ todo, updateTodo, confirmTodo }}>
{ children }
</NewTodoContext.Provider>
)
}
The AllTodosProvider would look like
function AllTodosProvider({ children }){
const [todos, setTodos] = React.useState();
const addTodo = React.useCallback((todo) => {
setTodos([...todos, todo]);
},[todos]);
return (
<AllTodosContext.Provider value={{ todos, addTodo }}>
{ children }
</AllTodosContext.Provider>
)
}
and the React tree would be like
<AllTodosProvider>
<DifferentComponents />
<NewTodoButton />
<DifferentComponents />
</AllTodosProvider>
// NewTodoButton would open up into
<NewTodoProvider>
<SomeFormComponent />
</NewTodoProvider>
Now coming to the issue I am facing. As you can see, the confirmTodo is wrapped in useCallback with todo as a dependency so that it has the latest state of NewTodoProvider, but that doesn't seem to be the case. When confirmTodo is called, it seems to pick up the default state, hinting that its implementation remained unchanges inspite of state changes. What am I missing here?

Related

React Native + Context + FlashList wont re-render with Context update + extraData updating

The problem: I have a FlashList that uses React Context to fill in the data (the data is an array of objects that renders a View) but when I update the context and the extraData prop for FlashList, the list does not re-render, or re-renders sometimes, or takes multiple events to actually re-render.
The Code:
// Many imports, they are all fine though
export default () => {
// Relevant context.
const {
cardsArray,
cardsArrayFiltered,
updateCardsArray,
updateCardsArrayFiltered
} = useContext(AppContext);
// Relevant state.
const [didUpdateCards, setDidUpdateCards] = useState(false);
const [cardsFilters, setCardsFilters] = useState([]);
// Relevant refs.
const flatListRef = useRef(null);
// Example effect on mount
useEffect(() => {
setInitialAppState();
}, []);
// Effect that listen to changing on some data that update the context again
useEffect(() => {
const newCardsArray = doSomeFiltering(cardsArray, cardsFilters);
updateCardsArrayFiltered(newCardsArray);
setDidUpdateCards(!didUpdateCards);
}, [cardsFilters]);
// Example of promisey function that sets the initial context.
const setInitialAppState = async () => {
try {
const newCardsArray = await getPromiseyCards();
updateCardsArrayFiltered(newCardsArray);
updateCardsArray(newCardsArray);
} catch ( err ) {
console.debug( err );
}
}
// Renderer for the list item.
const renderListItem = useCallback((list) => <Card key={list.index} card={list.item} />, []);
// List key extractor.
const listKeyExtractor = useCallback((item) => item.id, []);
return (
<FlashList
ref={flatListRef}
data={cardsArrayFiltered}
extraData={didUpdateCards}
keyExtractor={listKeyExtractor}
renderItem={renderListItem}
showsVerticalScrollIndicator={false}
estimatedItemSize={Layout.window.height}
/>
);
}
Notes:
What I did not write all out is the function, logic, view to update cardsFilters however the above effect IS running when it changes.
Moreover, this line here, const newCardsArray = doSomeFiltering(cardsArray, cardsFilters); does indeed return the proper updated data.
What's going on here? I am updating the extraData prop with that didUpdateCards state when the context changes which I thought was the requirement to re-render a FlatList/FlashList.
It looks like object being passed as extraData is a boolean. This means that if the previous value was true, setting it as true again wouldn't count as a change. Instead use an object and update it when you want list to update.
To try just set extraData={{}}. if everything works as expected it means that your update logic has some problem.

MUI Popper Component with Props keeps closing component on re-render

I am curious how to architect a component leveraging MUI's Popover component when there are dynamic props getting passed to a controlled Slider component inside of the Popover component — as well as the anchor element also getting dynamically updated as the value changes getting passed-down from a higher order component.
What is happening is that when the controlled child is updated by the user, it dispatches the change higher up the chain, driving new values down, which then re-renders the component, setting the anchorEl back to null. Here's a quick video in action:
I'm sure there is something straightforward I could do to avoid this. Any help is appreciated!
Here is abbreviated code:
function Component({ dynamicProps }) {
const [anchorEl, setAnchorEl] = React.useState(null);
const { dispatch } = useContext();
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleChange = (_, newValue) => {
dispatch({
body: newValue
});
};
const open = Boolean(anchorEl);
const id = open ? "simple-popover" : undefined;
return (
<div>
<Button
onClick={handleClick}
label={dynamicProps.label}
></Button>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
>
<Box sx={{ minWidth: "200px", mx: 2 }}>
<Slider
value={dynamicProps.value}
onChange={handleChange}
/>
</Box>
</Popover>
</div>
);
}
I have tried separating the Slider into another component, to avoid the re-render, and using my context's state to grab the values that I need, hover that point seems moot, since I still need to reference the anchorEl in the child, and since the trigger also is leveraging dynamic props, it will re-render and keep null-ing the anchorEl.
Ok team. Figured this one all-by-myself 🤗
Here's what you don't want to do: If you're going to use context — use it both for dispatching and grabbing state. Don't drill-down state from a parent component that will trigger a re-render. For both the button label and the controlled Slider, as long as you use the state insider the Component function through your context hook, you won't trigger a re-render, making your popper disappear from the re-render.
Do this 👇
export default function Assumption({ notDynamicProps }) {
const [anchorEl, setAnchorEl] = React.useState(null);
const { dispatch, state } = useRentalCalculator();
Not this 👇
export default function Assumption({ dynamicProps, notDynamicProps }) {
const [anchorEl, setAnchorEl] = React.useState(null);
const { dispatch } = useRentalCalculator();

Props in child doesn't update when parent updates it's state

I've spent a few days on this and it is driving me crazy now.
I have a state in a parent component containing an Array[string] of selected squares which is passed to the child component (a map) along with the set function from the hook. The issue is that when I set the new squares they are changed in the parent, but on selection of another square it is not taking into account the already selected squares.
function Parent(props){
const [selectedSquares, setSquares] = useState([]);
useEffect(() => {
console.log('parent useEffect', selectedSquares);
}, [selectedSquares]);
return (
<Child selectedSquares={selectedSquares}
handleSquaresChange={setSquares}
/>
)
}
function Child(props){
const {selectedSquares, handleSquaresChange} = props;
useEffect(() => {
console.log('child useEffect', selectedSquares)
}, [selectedSquares]);
const handleSelect = evt => {
if(evt.target){
const features = evt.target.getFeatures().getArray();
let selectedFeature = features.length ? features[0] : null;
if (selectedFeature) {
console.log('select (preadd):', selectedSquares);
const newTile = selectedFeature.get('TILE_NAME');
const newSquares = [...selectedSquares];
newSquares.push(newTile);
const newTest = 'newTest';
handleSquaresChange(newSquares);
console.log('select (postadd):', newSquares);
}
}
return(
<Map>
<Select onSelect={handleSelect}/>
</Map>
)
}
On the first interactionSelect component I get this output from the console:
parent useEffect: [],
child useEffect: [],
select (preadd):[],
child useEffect:['NX'],
parent useEffect: ['NX'],
select (postadd): ['NX'].
Making the second selection this is added to the console:
select (preadd):[],
select (postadd): ['SZ'],
child useEffect:['SZ'],
parent useEffect: ['SZ'].
Turns out there is an addEventListener in the library I am using that is going wrong. Thanks to everyone who responded but turns out the issue was not with React or the state stuff.
Consider something like the code below. Your parent has an array with all your options. For each option, you render a child component. The child component handles the activity of its own state.
function Parent(props){
// array of options (currently an array of strings, but this can be your squares)
const allOptions = ['opt 1', 'opt 2', 'opt 3', 'etc'];
return (
<>
// map over the options and pass option to child component
{allOptions.map((option) => <Child option={option}/>)}
</>
)
}
function Child({ option }){
const [selected, setSelected] = useState(false); // default state is false
return (
<>
// render option value
<p>{option}</p>
// shows the state as selected or not selected
<p>Option is: {selected ? "selected" : "not selected"}</p>
// this button toggles the active state
<button onClick={() => setSelected(!selected)}>Toggle</button>
</>
)
}

export Hooks in React for Nested Components?

I'm exporting hooks with nested components so that the parent can toggle state of a child. How can I make this toggle work with hooks instead of classic classes or old school functions?
Child Component
export let visible;
export let setVisible = () => {};
export const ToggleSwitch = () => {
const [visible, setVisibile] = useState(false);
return visible && (
<MyComponent />
)
}
Parent
import * as ToggleSwitch from "ToggleSwitch";
export const Parent: React.FC<props> = (props) => {
return (
<div>
<button onClick={() => ToggleSwitch.setVisible(true)} />
</div>
)
}
Error: Linter says [setVisible] is unused variable in the child... (but required in the parent)
You can move visible state to parent like this:
const Child = ({ visible }) => {
return visible && <h2>Child</h2>;
};
const Parent = () => {
const [visible, setVisible] = React.useState(false);
return (
<div>
<h1>Parent</h1>
<Child visible={visible} />
<button onClick={() => setVisible(visible => !visible)}>
Toggle
</button>
</div>
);
};
If you have many child-components you should make more complex logic in setVisible. Put object to useState where properties of that object will be all names(Ids) of child-components
as you know React is one-way data binding so if you wanna pass any props or state you have only one way to do that by passing it from parent to child component and if the logic becomes bigger you have to make it as a global state by using state management library or context API with react hooks use reducer and use effect.

Why is the initial state being used when I try to update state

I have a react component that uses hooks for state. I have set the initial state for home to {location:null, canCharge: 'yes'}.
I then have a couple of subcomponents that call setHome() to update the pieces of the state they are responsible for.
One sets the location, and the other sets the canCharge property of the home state.
The setter for the ChargeRadioGroup works as expected, only updating the canCharge property and has no effect on the value of location.
The PlacesAutoComplete set however seems to have captured the initial state of home, and after setting a breakpoint inside, I see that it always is called with home: {location:null, canCharge:'yes'}.
I realize I could break this single state into two separate states, one for location and one for canCharge, but I'd like to understand why this is happening instead of implementing a workaround.
export default function VerticalLinearStepper() {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const [home, setHome] = useState({
location: null,
canCharge: "yes"
});
const [work, setWork] = useState({
location: null,
canCharge: "yes"
});
const steps = getSteps();
const handleNext = () => {
setActiveStep(prevActiveStep => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep(prevActiveStep => prevActiveStep - 1);
};
return (
<div className={classes.root}>
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepLabel>Where do you live?</StepLabel>
<StepContent>
<Box className={classes.stepContent}>
<PlacesAutocomplete
className={classes.formElement}
name={"Home"}
onPlaceSelected={location => setHome({ ...home, location })}
googleApiKey={"<API_KEY>"}
/>
<ChargeRadioGroup
className={classes.formElement}
label="Can you charge your car here?"
value={home.canCharge}
onChange={event =>
setHome({ ...home, canCharge: event.target.value })
}
/>
The code for the PlacesAutoComplete component can be seen here
I'm guessing this has something to do with the way that this component calls it's onPlaceSelected prop, but I can't figure out exactly what's going on, or how to fix it:
useEffect(() => {
if (!loaded) return;
const config = {
types,
bounds,
fields
};
if (componentRestrictions) {
config.componentRestrictions = componentRestrictions;
}
autocomplete = new window.google.maps.places.Autocomplete(
inputRef.current,
config
);
event = autocomplete.addListener("place_changed", onSelected);
return () => event && event.remove();
}, [loaded]);
const onSelected = () => {
if (onPlaceSelected && autocomplete) {
onPlaceSelected(autocomplete.getPlace());
}
};
Updating my original answer.
Instead of this:
onPlaceSelected={location => setHome({ ...home, location })}
This:
onPlaceSelected={newlocation => setHome( (prevState) => (
{ ...prevState, location:newlocation }
))}
The set state functions can take a value, and object or a function that receives the old state and returns the new state. Because setting state is sometimes asynchronous, object state with members getting set with different calls may result in captured variables overwriting new state.
More details at this link: https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3

Categories