I can create a class component with 2 buttons, "A" and "B". Button A should set a state variable and then take some action. Button B should also set the same state variable and then take a different action. They key thing that this hinges on is the second argument to setState, which is the callback function for what action to take after the state has been set.
When I try to write this as a function component, I run into an issue. I move the callbacks into a hook (or multiple hooks), but I have no way to know which button was clicked. Furthermore, the hook does not trigger every time I click the button. The hook only triggers when the button results in a real state change (e.g. changing 5 to 5 skips the effect).
Here is a simple example:
import React from 'react';
import './styles.css';
// actionA and actionB represent things that I want to do when the
// buttons are clicked but after the corresponding state is set
const actionA = () => {
console.log('button A thing');
};
const actionB = () => {
console.log('button B thing');
};
// render 2 components that should behave the same way
const App = () => {
return (
<>
<ClassVersion />
<FunctionVersion />
</>
);
};
// the class version uses this.state
class ClassVersion extends React.Component {
constructor(props) {
super(props);
this.state = {
x: 0,
};
}
render = () => {
return (
<div className='box'>
Class Version
<br />
<button
type='button'
onClick={() => {
this.setState({ x: 1 }, () => {
// here I can do something SPECIFIC TO BUTTON A
// and it will happen AFTER THE STATE CHANGE
actionA();
});
// can't call actionA here because the
// state change has not yet occurred
}}
>
A
</button>
<button
type='button'
onClick={() => {
this.setState({ x: 2 }, () => {
// here I can do something SPECIFIC TO BUTTON B
// and it will happen AFTER THE STATE CHANGE
actionB();
});
// can't call actionB here because the
// state change has not yet occurred
}}
>
B
</button>
State: {this.state.x}
</div>
);
};
}
// the function version uses the useState hook
const FunctionVersion = () => {
const [x, setX] = React.useState(0);
React.useEffect(() => {
// the equivalent "set state callback" does not allow me to
// differentiate WHY X CHANGED. Was it because button A was
// clicked or was it because button B was clicked?
if (/* the effect was triggered by button A */ true) {
actionA();
}
if (/* the effect was triggered by button B */ true) {
actionB();
}
// furthermore, the effect is only called when the state CHANGES,
// so if the button click does not result in a state change, then
// the EFFECT (AND THUS THE CALLBACK) IS SKIPPED
}, [x]);
return (
<div className='box'>
Function Version
<br />
<button
type='button'
onClick={() => {
// change the state and trigger the effect
setX(1);
// can't call actionA here because the
// state change has not yet occurred
}}
>
A
</button>
<button
type='button'
onClick={() => {
// change the state and trigger the effect
setX(2);
// can't call actionB here because the
// state change has not yet occurred
}}
>
B
</button>
State: {x}
</div>
);
};
export default App;
I came up with a couple ideas:
Change the type of x from number to {value: number; who: string}. Change calls from setX(1) to setX({value: 1, who: 'A'}). This will allow the effect to know who triggered it. It will also allow the effect to run every time the button is clicked because x is actually getting set to a new object each time.
Change the type of x from number to {value: number; callback: () => void}. Change calls from setX(1) to setX({value: 1, callback: actionA}). Same as above, but slightly different. The effect would say if(x.callback) { x.callback() }.
Both these ideas seem to work, but the problem is I don't want to keep adding an extra tag to my state variables just to keep track of which thing triggered them.
Is my idea the correct solution, or is there a better way to do this that doesn't involve "tagging" my state variables?
How about this:
const [a, setA] = React.useState(0);
const [b, setB] = React.useState(0);
React.useEffect(() => {
actionA();
}, [a]);
React.useEffect(() => {
actionB();
}, [b]);
onClick={() => {
setX(1);
setA(a => a + 1);
}}
onClick={() => {
setX(2);
setB(b => b + 1);
}}
Let's say you put actionA and actionB in the corresponding button A/B onClick(). Given that you WILL be updating state based on the action of this onClick, you could directly call the action functions during the onClick if you pass the new state data to the functions.
onClick={() => {
actionA(newStateData);
setX(newStateData);
}}
If you don't want to do this, you could use:
your solution of adding an identifier to the state value would be fine!
Create separate state values for A and B
Related
I want to use a state variable (value) when a modal is closed. However, any changes made to the state variable while the modal is open are not observed in the handler. I don't understand why it does not work.
CodeSandbox
or
Embedded CodeSandbox
Open the modal
Click 'Set value'
Click 'Hide modal'
View console log.
My understanding is that the element is rendered when the state changes (Creating someClosure foo), but then when the closure function is called after that, the value is still "". It appears to me to be a "stale value in a closure" problem, but I can't see how to fix it.
I have looked at explanations regarding how to use useEffect, but I can't see how they apply here.
Do I have to use a useRef or some other way to get this to work?
[Edit: I have reverted the React version in CodeSandbox, so I hope it will run now. I also implemented the change in the answers below, but it did not help.]
import { useState } from "react";
import { Modal, Button } from "react-materialize";
import "./styles.css";
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState("");
console.log("Creating someClosure value =", value);
const someClosure = (argument) => {
console.log("In someClosure value =", value);
console.log("In someClosure argument =", argument);
setIsOpen(false);
};
return (
<div className="App">
<Button onClick={() => setIsOpen(true)}>Show modal</Button>
<Modal open={isOpen} options={{ onCloseStart: () => someClosure(value) }}>
<Button onClick={() => setValue("foo")}>Set value</Button>
<Button onClick={() => setIsOpen(false)}>Hide modal</Button>
</Modal>
</div>
);
}
This is a hard concept. You are using into your member function a state which evaluates "" at render so regardless state change the function signature still the same before render this is the reason why useEffect and useCallback should be used to trait side effects about state change. But there are a way to ensure get correct state without hooks. Just passing state as a parameter to function, by this approach you will receive the current state at render so just with few changes you achieve this.
At someClosure just create an argument:
const someClosure = (value) => {...}
So into modal component,
options={{ onCloseStart: someClosure(value) }}
Should be what you are looking for
Issue
The issue here is that you've declared a function during some render cycle and the current values of any variable references are closed over in scope:
const someClosure = () => {
console.log("In someClosure value =", value); // value -> ""
setIsOpen(false);
};
This "instance" of the callback is passed as a callback to a component and is invoked at a later point in time:
<Modal open={isOpen} options={{ onCloseStart: someClosure }}>
<Button onClick={() => setValue("foo")}>Set value</Button>
<Button onClick={() => setIsOpen(false)}>Hide modal</Button>
</Modal>
When the modal is triggered to close the callback with the now stale closure over the value state value is called.
Solution
Do I have to use a useRef or some other way to get this to work?
Basically yes, use a React ref and a useEffect hook to cache the state value that can be mutated/accessed at any time outside the normal React component lifecycle.
Example:
import { useEffect, useRef, useState } from "react";
...
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState("");
const valueRef = useRef(value);
useEffect(() => {
console.log("Creating someClosure value =", value);
valueRef.current = value; // <-- cache current value
}, [value]);
const someClosure = (argument) => {
console.log("In someClosure value =", valueRef.current); // <-- access current ref value
console.log("In someClosure argument =", argument);
setIsOpen(false);
};
return (
<div className="App">
<Button onClick={() => setIsOpen(true)}>Show modal</Button>
<Modal
open={isOpen}
options={{
onCloseStart: () => someClosure(valueRef.current) // <-- access current ref value
}}
>
<Button onClick={() => setValue("foo")}>Set value</Button>
<Button onClick={() => setIsOpen(false)}>Hide modal</Button>
</Modal>
</div>
);
}
Although Drew's solution has solved the problem, but this proplem is actually caused by <Model> element which use options to pass callback function which has been resolved at first render. element don't update their options in the later rendering. This should be a bug.
In Drew's solution.
options={{
onCloseStart: () => someClosure(valueRef.current) // <-- access current ref value
}}
this callback's argument is a ref object which has similar to a pointer. when the ref's current changed, it looks like the value is not stalled.
You can verify by add:
onClick={()=>someClosure(value)}
in the <Model> element and you will see the value is updated.
This is a interesting problem, so I check the <Model> element source code in Github:
useEffect(() => {
const modalRoot = _modalRoot.current;
if (!_modalInstance.current) {
_modalInstance.current = M.Modal.init(_modalRef.current, options);
}
return () => {
if (root.contains(modalRoot)) {
root.removeChild(modalRoot);
}
_modalInstance.current.destroy();
};
// deep comparing options object
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [safeJSONStringify(options), root]);
You can find that the author use SafeJSONStringify(options) to do a deep comparing which don't care any state's value change.
I'm playing around with a hook that can store some deleted values. No matter what I've tried, I can't get the state from this hook to update when I use it in a component.
const useDeleteRecords = () => {
const [deletedRecords, setDeletedRecords] = React.useState<
Record[]
>([]);
const [deletedRecordIds, setDeletedRecordIds] = React.useState<string[]>([]);
// ^ this second state is largely useless – I could just use `.filter()`
// but I was experimenting to see if I could get either to work.
React.useEffect(() => {
console.log('records changed', deletedRecords);
// this works correctly, the deletedRecords array has a new item
// in it each time the button is clicked
setDeletedRecordIds(deletedRecords.map((record) => record.id));
}, [deletedRecords]);
const deleteRecord = (record: Record) => {
console.log(`should delete record ${record.id}`);
// This works correctly - firing every time the button is clicked
setDeletedRecords(prev => [...prev, record]);
};
const wasDeleted = (record: Record) => {
// This never works – deletedRecordIds is always [] when I call this outside the hook
return deletedRecordIds.some((r) => r === record.id);
};
return {
deletedRecordIds,
deleteRecord,
wasDeleted,
} // as const <-- no change
}
Using it in a component:
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecordIds, wasDeleted, deleteRecord } = useDeleteRecords();
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
}
React.useEffect(() => {
console.log('should fire when deletedRecordIds changes', deletedRecordIds);
// Only fires once for each row on load? deletedRecordIds never changes
// I can rip out the Ids state and do it just with deletedRecords, and the same thing happens
}, [deletedRecordIds]);
}
If it helps, these are in the same file – I'm not sure if there's some magic to exporting a hook in a dedicated module? I also tried as const in the return of the hook but no change.
Here's an MCVE of what's going on: https://codesandbox.io/s/tender-glade-px631y?file=/src/App.tsx
Here's also the simpler version of the problem where I only have one state variable. The deletedRecords state never mutates when I use the hook in the parent component: https://codesandbox.io/s/magical-newton-wnhxrw?file=/src/App.tsx
problem
In your App (code sandbox) you call useDeleteRecords, then for each record you create a DisplayRecord component. So far so good.
function App() {
const { wasDeleted } = useDeleteRecords(); // ✅
console.log("wtf");
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord record={record} /> // ✅
</div>
) : null;
})}
</div>
);
}
Then for each DisplayRecord you call useDeleteRecords. This maintains a separate state array for each component ⚠️
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecords, deleteRecord } = useDeleteRecords(); // ⚠️
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
};
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
// Only fires once for each row on load? deletedRecords never changes
}, [deletedRecords]);
return (
<div>
<div>{record.id}</div>
<div onClick={handleDelete} style={{ cursor: "pointer" }}>
[Del]
</div>
</div>
);
};
solution
The solution is to maintain a single source of truth, keeping handleDelete and deletedRecords in the shared common ancestor, App. These can be passed down as props to the dependent components.
function App() {
const { deletedRecords, deleteRecord, wasDeleted } = useDeleteRecords(); // 👍🏽
const handleDelete = (record) => (event) { // 👍🏽 delete handler
deleteRecord(record);
};
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord
record={record}
deletedRecords={deletedRecords} // 👍🏽 pass prop
handleDelete={handleDelete} // 👍🏽 pass prop
/>
</div>
) : null;
})}
</div>
);
}
Now DisplayRecord can read state from its parent. It does not have local state and does not need to call useDeleteRecords on its own.
const DisplayRecord = ({ record, deletedRecords, handleDelete }) => {
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
}, [deletedRecords]); // ✅ passed from parent
return (
<div>
<div>{record.id}</div>
<div
onClick={handleDelete(record)} // ✅ passed from parent
style={{ cursor: "pointer" }}
children="[Del]"
/>
</div>
);
};
code demo
I would suggest a name like useList or useSet instead of useDeleteRecord. It's more generic, offers the same functionality, but is reusable in more places.
Here's a minimal, verifiable example. I named the delete function del because delete is a reserved word. Run the code below and click the ❌ to delete some items.
function App({ items = [] }) {
const [deleted, del, wasDeleted] = useSet([])
React.useEffect(_ => {
console.log("an item was deleted", deleted)
}, [deleted])
return <div>
{items.map((item, key) =>
<div className="item" key={key} data-deleted={wasDeleted(item)}>
{item} <button onClick={_ => del(item)} children="❌" />
</div>
)}
</div>
}
function useSet(iterable = []) {
const [state, setState] = React.useState(new Set(...iterable))
return [
Array.from(state), // members
newItem => setState(s => (new Set(s)).add(newItem)), // addMember
item => state.has(item) // isMember
]
}
ReactDOM.render(
<App items={["apple", "orange", "pear", "banana"]}/>,
document.querySelector("#app")
)
div.item { display: inline-block; border: 1px solid dodgerblue; padding: 0.25rem; margin: 0.25rem; }
[data-deleted="true"] { opacity: 0.3; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Since you are updating deletedRecordIds inside a React.useEffect, this variable will have the correct value only after the render complete. wasDeleted is a closure that capture the value of deletedRecordIds when the component renders, thus it always have a stale value. As yourself are suggesting, the correct way to do that is to use .filter() and remove the second state.
Talking about the example you provided in both cases you are defining 5 hooks: one hook for each DisplayRecord component and one for the App. Each hook define is own states, thus there are 5 deletedRecords arrays on the page. Clicking on Del, only the array inside that specific component will be updated. All other component won't be notified by the update, because the state change is internal to that specific row. The hook state in App will never change because no one is calling its own deleteRecord function.
You could solve that problem in 2 way:
Pulling up the state: The hook is called just once in the App component and the deleteRecord method is passed as parameter to every DisplayRecord component. I updated your CodeSandbox example.
Use a context: Context allows many component to share the same state.
I created a state variable using
const [drawing, setDrawing] = useState(false)
Then I've this button which should update the value of drawing
<button onClick={() => toggleDrawing}>Toggle Drawing</button>
The toggleDrawing function is:
const toggleDrawing = () => {
setDrawing(true)
}
but when I press the button and console.log the value of drawing id doesn't update, it's always a step behind. it start with a value of false, after one click it remains false and after the second click it switches to true.
const toggleDrawing = () => {
setDrawing((oldValue) => !oldValue)
}
if you want to toggle state in React you always want to refer to the previous state
const [drawing, setDrawing] = useState(false)
Case:1 If you want a toggle true=>false and false=>true
const toggleDrawing = () => {
setDrawing(!drawing);
}
<button onClick={toggleDrawing}>Toggle Drawing</button>
Case:2 If you just want to set it to true
const toggleDrawing = () => {
setDrawing(true);
}
<button onClick={toggleDrawing}>Toggle Drawing</button>
Sandbox https://codesandbox.io/s/vigilant-meadow-thdni?file=/src/App.js
Note => console.log(drawing) in toggleDrawing function will return false at first then true reason is state is not updated
The Problem
You're just returning the toggleDrawing function without executing it
Solution
Execute the function when the click event occurs like this
<button onClick={() => toggleDrawing()}>Toggle Drawing</button>
Or pass the toggleDrawing to onClick as the actual function like
<button onClick={toggleDrawing}>Toggle Drawing</button>
I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.
I have an array of objects in which each object has an onclick event. I map my array of objects to a component. I have a button in which I change a variable's value.
If I click my button, it changes the variable's value, but when I fire the onclick event on one of my components it does not use the variable's changed value. (I'm guessing that this is normal behaviour because the event was already binded to the component when it was created.)
Although, how can I use my updated variable value in my component's onClick event?
First I declare my variables
const [myVariable, setMyVariable] = useState("first value");
const [mymap, setmymap] = useState([
{
key: 1,
name: "one",
onClick: event => handleClick(event)
},
{
key: 2,
name: "two",
onClick: event => handleClick(event)
}
]);
setVariable is my onClick event of my first button that changes my variable's value
const setVariable = () => {
setMyVariable("second value");
};
handleClick is the onClick event of my components
const handleClick = () => {
console.log(myVariable);
};
Here I return my UI with my map of MyComponent
return (
<div>
<button onClick={setVariable}>Set variable</button>
<h1>{myVariable}</h1>
{mymap.map(element => (
<MyComponent key={element.key} onclickevent={element.onClick} />
))}
</div>
);
In this very example storing the event in every mymap item looks redundant. If there is no reason for it then simpler version will work as expected:
import React, { useState, useCallback } from "react";
const MyComponent = ({onclickevent}) => (<div>
<button onClick={onclickevent}>
Use variable from MyComponent
</button>
</div>);
const MyPage = props => {
const [myVariable, setMyVariable] = useState("first value");
// handler changes only when `myVariable` changes
const clickHandler = useCallback(() => {
console.log(myVariable);
}, [myVariable]);
// removing handlers from state
const [mymap, setmymap] = useState([
{
key: 1,
name: "one"
},
{
key: 2,
name: "two"
}
]);
return (
<div>
<button onClick={() => setMyVariable("second value")}>Set another value</button>
<h1>{myVariable}</h1>
{mymap.map(element => (
<MyComponent key={element.key} onclickevent={clickHandler} />
))}
</div>
);
};
export default MyPage;
Above instead storing handler in a state and then obtaining it when iterating over state elements mymap.map(el => el.onClick) it is directly passed to children (example shows those are all the same anyway). But we also need to make sure that handler is not stale and changes depending on the value it expects:
const clickHandler = useCallback(() => {
console.log(myVariable);
}, [myVariable]);
Updated Codesandbox example provided in a question:
PouncingPoodle, I stuck with answering by myself. But I've just found
the answer which fits perfectly for your trouble
Thanks so much #Travnikov.dev for the perfect point.
Here is my solution:
useEffect(() => {
setmymap(prev => {
let update = [...prev];
for (let i = 0; i < update.length; i ++) {
update[i].onClick = (event => handleClick(event))
update[i] = {...update[i], onClick: update[i].onClick}
}
return update;
})
}, [myVariable]);
So what's happening is I have an useEffect with the dependency of myVariable. Once my value has changed, I just update my components' array of objects's onClick to exactly what is was, except this time the onClick will have the updated value.