Isn't the hook useCallback supposed to return an updated function every time a dependency change?
I wrote this code sandbox trying to reduce the problem I'm facing in my real app to the minimum reproducible example.
import { useCallback, useState } from "react";
const fields = [
{
name: "first_name",
onSubmitTransformer: (x) => "",
defaultValue: ""
},
{
name: "last_name",
onSubmitTransformer: (x) => x.replace("0", ""),
defaultValue: ""
}
];
export default function App() {
const [instance, setInstance] = useState(
fields.reduce(
(acc, { name, defaultValue }) => ({ ...acc, [name]: defaultValue }),
{}
)
);
const onChange = (name, e) =>
setInstance((instance) => ({ ...instance, [name]: e.target.value }));
const validate = useCallback(() => {
Object.entries(instance).forEach(([k, v]) => {
if (v === "") {
console.log("error while validating", k, "value cannot be empty");
}
});
}, [instance]);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
setInstance((instance) =>
fields.reduce(
(acc, { name, onSubmitTransformer }) => ({
...acc,
[name]: onSubmitTransformer(acc[name])
}),
instance
)
);
validate();
},
[validate]
);
return (
<div className="App">
<form onSubmit={onSubmit}>
{fields.map(({ name }) => (
<input
key={`field_${name}`}
placeholder={name}
value={instance[name]}
onChange={(e) => onChange(name, e)}
/>
))}
<button type="submit">Create object</button>
</form>
</div>
);
}
This is my code. Basically it renders a form based on fields. Fields is a list of objects containing characteristics of the field. Among characteristic there one called onSubmitTransformer that is applied when user submit the form. When user submit the form after tranforming values, a validation is performed. I wrapped validate inside a useCallback hook because it uses instance value that is changed right before by transform function.
To test the code sandbox example please type something is first_name input field and submit.
Expected behaviour would be to see in the console the error log statement for first_name as transformer is going to change it to ''.
Problem is validate seems to not update properly.
This seems like an issue with understanding how React lifecycle works. Calling setInstance will not update instance immediately, instead instance will be updated on the next render. Similarly, validate will not update until the next render. So within your onSubmit function, you trigger a rerender by calling setInstance, but then run validate using the value of instance at the beginning of this render (before the onSubmitTransformer functions have run).
A simple way to fix this is to refactor validate so that it accepts a value for instance instead of using the one from state directly. Then transform the values on instance outside of setInstance.
Here's an example:
function App() {
// setup
const validate = useCallback((instance) => {
// validate as usual
}, []);
const onSubmit = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const transformedInstance = fields.reduce((acc, {name, onSubmitTransformer}) => ({
...acc,
[name]: onSubmitTransformer(acc[name]),
}), instance);
setInstance(transformedInstance);
validate(transformedInstance);
}, [instance, validate]);
// rest of component
}
Now the only worry might be using a stale version of instance (which could happen if instance is updated and onSubmit is called in the same render). If you're concerned about this, you could add a ref value for instance and use that for submission and validation. This way would be a bit closer to your current code.
Here's an alternate example using that approach:
function App() {
const [instance, setInstance] = useState(/* ... */);
const instanceRef = useRef(instance);
useEffect(() => {
instanceRef.current = instance;
}, [instance]);
const validate = useCallback(() => {
Object.entries(instanceRef.current).forEach(([k, v]) => {
if (v === "") {
console.log("error while validating", k, "value cannot be empty");
}
});
}, []);
const onSubmit = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const transformedInstance = fields.reduce((acc, {name, onSubmitTransformer}) => ({
...acc,
[name]: onSubmitTransformer(acc[name]),
}), instanceRef.current);
setInstance(transformedInstance);
validate(transformedInstance);
}, [validate]);
// rest of component
}
Related
I am making a kind of input in react.
I have the following
=> A component returning a onSelectionChange that return an array of selected element (works fine)
useEffect(() => {
onSelectionChange?.(selection.items);
}, [selection]);
=> A input that wrap the above component and contain a button save/cancel. When use click save, I want to dispatch a onChange with the latest value selected. If user click cancel, it will reset to initial value. ( I intentionally removed some of the component code to keep the problematic parts)
export const VariantsInput = forwardRef<any, any>(
({ value = [], ...rest }, ref) => {
//....
const [selectedProducts, setSelectedProducts] = useState<VariantSetDetail[]>(value);
const onValidate = useCallback(() => {
console.log(selectedProducts); // always the previous state of selectedProducts
onChange(selectedProducts);
closeDialog(dialogId);
}, [selectedProducts]);
useEffect(() => {
console.log(selectedProducts); // always the latest state of selectedProducts
}, [selectedProducts]);
//...
<VariantSelector
selectedSku={value}
onSelectionChange={(selection) => {
setSelectedProducts(() => [
...selection.map((selected) => {
const alreadySelected = value.find(
(product) =>
product.productVariantId === selected.productVariantId
);
return alreadySelected ? alreadySelected : selected;
}),
]);
}}
/>
<button onClick={onValidate}> Save</button>
//....
The issue is selectedProducts is always the previous state in the onValidate. If I check all my events onSelectionChange, or put log inside the effect and the root of the component to display the state, it contains exactly what I want. But in the on validate, it is always []....
I am really confused as to why.
I have this component:
import "./styles.css";
import React, { useEffect, useRef, useState } from "react";
import JSONEditor from "jsoneditor";
import "jsoneditor/dist/jsoneditor.css";
const JSONReact = ({ json, mode, onChange }) => {
const ref1 = useRef(null);
const ref2 = useRef(null);
useEffect(() => {
const props = {
onChangeText: (value) => {
console.log(value, "vv");
onChange(value);
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
if (json) {
ref1.current.set(json);
}
return () => {
ref1.current.destroy();
};
}, []);
useEffect(() => {
if (json) {
ref1?.current.update(json);
}
}, [json]);
return <div ref={ref2} />;
};
export default function App() {
const [state, setState] = useState('{"cars": "22w-08w-23"}');
const onChange = (j) => {
console.log(j);
setState(j);
};
return (
<div className="App">
<JSONReact mode="code" onChange={onChange} json={JSON.parse(state)} />
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
When i type something inside editor i get undefined in console.log(j);, but i don't understand why. Who can help to fix it?
https://codesandbox.io/s/admiring-khorana-pdem1l?file=/src/App.js:909-929
Well it is expected as in the docs it clearly says "This callback does not pass the changed contents", over here https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#:~:text=This%20callback%20does%20not%20pass%20the%20changed%20contents
However, There are two ways to do it, that I found from here
https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#configuration-options
First Way: use getText() from JSONEditor object to get the current value, since it will return you string so just parse it in json and pass inside your callback (onChange) prop.
JavaScript
const props = {
onChange: () => {
onChange(JSON.parse(ref1.current.getText()));
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
Second way: just use onChangeText and it will give the string in the callback,
const props = {
onChangeText: (value) => {
onChange(JSON.parse(value));
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
EDIT 1: added JSON validation check before calling onChange Props
const isValidJSON = (jsonString) => {
try {
JSON.parse(jsonString);
} catch (e) {
return false;
}
return true;
};
useEffect(() => {
const props = {
onChangeText: (value) => {
isValidJSON(value) && onChange(JSON.parse(value));
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
EDIT 2: adding error validation using validate(), here in this case we can remove isValidJSON(value) check from below code since we are already checking for JSON errors using validate().
onChangeText: (value) => {
const errors = ref1.current.validate();
errors.then((err) => {
if (!err.length) {
isValidJSON(value) && onChange(JSON.parse(value));
} else {
console.log(err);
}
});
}
I think it is a normal behaviour of JSONEditor, according to docs:
{function} onChange()
Set a callback function triggered when the contents of the JSONEditor
change. This callback does not pass the changed contents, use get() or
getText() for that. Note that get() can throw an exception in mode
text, code, or preview, when the editor contains invalid JSON. Will
only be triggered on changes made by the user, not in case of
programmatic changes via the functions set, setText, update, or
updateText. See also callback functions onChangeJSON(json) and
onChangeText(jsonString). `
I'm just a react beginner. I'm trying to create a custom hook, which will be triggered once an onClick event is triggered. By what I see, I need to use the useRef hook, to take into account if the component is rendered by first time, or if it's being re-rendered.
My code approach is the next:
const Clear = (value) => {
const useClearHook = () => {
const stateRef = useRef(value.value.state);
console.log(stateRef);
useEffect(() => {
console.log("useEffect: ");
stateRef.current = value.value.state;
stateRef.current.result = [""];
stateRef.current.secondNumber = [""];
stateRef.current.mathOp = "";
console.log(stateRef.current);
value.value.setState({
...stateRef.current,
result: value.value.state.result,
secondNumber: value.value.state.secondNumber,
mathOp: value.value.state.mathOp,
});
}, [stateRef.current]);
console.log(value.value.state);
};
return <button onClick={useClearHook}>Clear</button>;
};
Any suggestion? Maybe I might not call ...stateRef.current in setState. I'm not sure about my mistake.
Any help will be appreciated.
Thanks!
Your problem is useClearHook is not a component (the component always goes with the first capitalized letter like UseClearHook), so that's why when you call useRef in a non-component, it will throw that error. Similarly, for useEffect, you need to put it under a proper component.
The way you're using state is also not correct, you need to call useState instead
Here is a possible fix for you
const Clear = (value) => {
const [clearState, setClearState] = useState()
const useClearHook = () => {
setClearState((prevState) => ({
...prevState,
result: [""],
secondNumber: [""],
mathOp: "",
}));
};
return <button onClick={useClearHook}>Clear</button>;
};
If your states on the upper component (outside of Clear). You can try this way too
const Clear = ({value, setValue}) => {
const useClearHook = () => {
setValue((prevState) => ({
...prevState,
result: [""],
secondNumber: [""],
mathOp: "",
}));
};
return <button onClick={useClearHook}>Clear</button>;
};
Here is how we pass it
<Clear value={value} setValue={setValue} />
The declaration for setValue and value can be like this in the upper component
const [value, setValue] = useState()
I'm struggling with React hooks using useEffect in case of storing tasks in localstorage, so refreshing page will still handle the elements from the list. The problem is while I'm trying to get the elements from LocalStorage and set them for todos state. They exist inside localStorage, but element that is inside todos state is only one from localstorage.
Here is a Component that handling this:
const [todos, setTodos] = useState([]);
const [todo, setTodo] = useState("");
const addTask = (event) => {
event.preventDefault();
let newTask = {
task: todo,
id: Date.now(),
completed: false,
};
setTodos([...todos, newTask]);
setTodo("");
};
const getLocalStorage = () => {
for (let key in localStorage) {
if (!localStorage.hasOwnProperty(key)) {
continue;
}
let value = localStorage.getItem(key);
try {
value = JSON.parse(value);
setTodos({ [key]: value });
} catch (event) {
setTodos({ [key]: value });
}
}
};
const saveLocalStorage = () => {
for (let key in todos) {
localStorage.setItem(key, JSON.stringify(todos[key]));
}
};
useEffect(() => {
getLocalStorage();
}, []);
useEffect(() => {
saveLocalStorage();
}, [saveLocalStorage]);
const testClick = () => {
console.log(todos);
};
return (
<div className="App">
<h1>What would you like to do?</h1>
<TodoForm
todos={todos}
value={todo}
inputChangeHandler={inputChangeHandler}
addTask={addTask}
removeCompleted={removeCompleted}
/>
{/* <TodoList todos={todos} toogleCompleted={toogleCompleted} /> */}
<button onClick={testClick}>DISPLAY TODOS</button>
</div>
);
Second problematic error while refreshing page:
Apart from that I can not display elements after getting them from local storage. Probably I'm missing some stupid little thing, but I stuck in this moment and I was wondering if small explanation would be possible where I'm making mistake.
I think you are overwriting the state value all together. You want to append to what exists in the todo state. You could use the spread operator to set the todo to everything that's already in there plus the new value.
Something like this:
setTodos({...todos, [key]: value });
setTodos([...todos, [key]: value ]);
Note that spread operator is a shallow clone. If you have a deeper object (more than two layers i think) then use something like cloneDeep from the lodash library.
To see whats happening change this block to add the console.logs
try {
value = JSON.parse(value);
setTodos({ [key]: value });
console.log(todos);
} catch (event) {
console.log('There was an error:', error);
// Should handle the error here rather than try to do what failed again
//setTodos({ [key]: value });
}
Edit: regarding the error you added.
You are trying to use map on an object. map is an array function.
Instead you would convert your object to an array and then back:
const myObjAsArr = Object.entries(todos).map(([k,v]) => {
// Do stuff with key and value
}
const backToObject = Object.fromEntries(myObjAsArr);
Change this to set an array (not object):
setTodos([...todos, [key]: value ]);
Put console logs everywhere so you can see whats happening.
I have this application that has a deprecated lifecycle method:
componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
Currently, I have used the UNSAFE_ flag:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
I have left it like this because when I attempted to refactor it to:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors) {
this._validate(prevProps, prevState);
}
}
It created another bug that gave me this error:
Invariant Violation: Maximum update depth exceeded. This can happen
when a component repeatedly calls setState inside componentWillUpdate
or componentDidUpdate. React limits the number of nested updates to
prevent infinite loops.
It starts to happen when a user clicks on the PAY NOW button that kicks off the _handlePayButtonPress which also checks for validation of credit card information like so:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
_validate = props => {
const { cardExpireDate, cardNumber, csv, nameOnCard } = props;
const validationErrors = {
date: cardExpireDate.trim() ? "" : "Is Required",
cardNumber: cardNumber.trim() ? "" : "Is Required",
csv: csv.trim() ? "" : "Is Required",
name: nameOnCard.trim() ? "" : "Is Required"
};
if (validationErrors.csv === "" && csv.trim().length < 3) {
validationErrors.csv = "Must be 3 or 4 digits";
}
const fullErrors = {
...validationErrors,
...this.props.validationErrors
};
const isValid = Object.keys(fullErrors).reduce((acc, curr) => {
if (fullErrors[curr]) {
return false;
}
return acc;
}, true);
if (isValid) {
this.setState({ validationErrors: {} });
//register
} else {
this.setState({ validationErrors, displayErrors: true });
}
return isValid;
};
_handlePayButtonPress = () => {
const isValid = this._validate(this.props);
if (isValid) {
console.log("Good to go!");
}
if (isValid) {
this.setState({ processingPayment: true });
this.props
.submitEventRegistration()
.then(() => {
this.setState({ processingPayment: false });
//eslint-disable-next-line
this.props.navigation.navigate("PaymentConfirmation");
})
.catch(({ title, message }) => {
Alert.alert(
title,
message,
[
{
text: "OK",
onPress: () => {
this.setState({ processingPayment: false });
}
}
],
{
cancelable: false
}
);
});
} else {
alert("Please correct the errors before continuing.");
}
};
Unfortunately, I do not have enough experience with Hooks and I have failed at refactoring that deprecated lifecycle method to one that would not create trouble like it was doing with the above error. Any suggestions at a better CDU or any other ideas?
You need another check so you don't get in an infinite loop (every time you call setState you will rerender -> component did update -> update again ...)
You could do something like this:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors && prevProps !== this.props) {
this._validate(prevProps, prevState);
}
}
Also I think that you need to call your validate with new props and state:
this._validate(this.props, this.state);
Hope this helps.
componentDidUpdate shouldn't replace componentWillRecieveProps for this reason. The replacement React gave us was getDerivedStateFromProps which you can read about here https://medium.com/#baphemot/understanding-react-react-16-3-component-life-cycle-23129bc7a705. However, getDerivedStateFromProps is a static function so you'll have to replace all the setState lines in _validate and return an object instead.
This is how you work with prevState and hooks.
Working sample Codesandbox.io
import React, { useState, useEffect } from "react";
import "./styles.css";
const ZeroToTen = ({ value }) => {
const [myValue, setMyValue] = useState(0);
const [isValid, setIsValid] = useState(true);
const validate = value => {
var result = value >= 0 && value <= 10;
setIsValid(result);
return result;
};
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
return (
<>
<span>{myValue}</span>
<p>
{isValid
? `${value} Is Valid`
: `${value} is Invalid, last good value is ${myValue}`}
</p>
</>
);
};
export default function App() {
const [value, setValue] = useState(0);
return (
<div className="App">
<button value={value} onClick={e => setValue(prevState => prevState - 1)}>
Decrement
</button>
<button value={value} onClick={e => setValue(prevState => prevState + 1)}>
Increment
</button>
<p>Current Value: {value}</p>
<ZeroToTen value={value} />
</div>
);
}
We have two components, one to increase/decrease a number and the other one to hold a number between 0 and 10.
The first component is using prevState to increment the value like this:
onClick={e => setValue(prevState => prevState - 1)}
It can increment/decrement as much as you want.
The second component is receiving its input from the first component, but it will validate the value every time it is updated and will allow values between 0 and 10.
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
In this case I'm using two hooks to trigger the validation every time 'value' is updated.
If you are not familiar with hooks yet, this may be confusing, but the main idea is that with hooks you need to focus on a single property/state to validate changes.