Hook
export const useCreateAccount = () => {
const [state, setState] = useState(initialState)
const onChangeInput: ChangeEventFunction = useCallback(({ target }) => {
if (!target.files) {
return setState({ ...state, [target.name]: target.value })
}
setState({ ...state, [target.name]: target.files[0] })
}, [])
return { onChangeInput }
}
Component
const { onChangeInput } = useCreateAccount()
<form>
<input name="name1" onChange={onChangeInput}>
<input name="name2" onChange={onChangeInput}>
</form>
Every time I do some change in second input(name2) the previous state(name1) of the component has been lost(reset to initial state), The reason I use 'useCallback', I only need one instance of 'onChangeInput'
But if I remove 'useCallback', state is keeping the previous values(name1)
I can't understand this behavior in hooks, can someone elaborate more on this?
From the docs:
Any function inside a component, including event handlers and effects, “sees” the props and state from the render it was created in.
Here, when you are using useCallback, the function has been defined in it's initial render and has the initial state defined then. This is the reason why useCallback has a depedency array that can be used to refresh the function and values used inside it.
But you cannot use state as a dependency because you are setting the same inside it, instead you can use the functional version of setState so as to get the previous values of state instead of reffering to the central one.
const onChangeInput: ChangeEventFunction = useCallback(({ target }) => {
if (!target.files) {
return setState(prevState => ({ ...prevState, [target.name]: target.value }));
}
setState(prevState => ({ ...prevState, [target.name]: target.files[0] }))
}, [])
Related
I'm converting a class component to functional component for practice. It has a ref object to contain some variables for the component, such as IntersectionObserver object to implement infinite scrolling.
The issue starts from here. The callback function of the IntersectionObserver calls a function(says update) defined in the component to load more data. Because the IntersectionObserver is defined inside the useRef, the update function is the function bound when the component gets initialized. So the value of the state that is used in the update function is also the value of the initial state.
How can I compose this functional component in a proper way?
Backbone demo
export default function A(props) {
const [state, setState] = useState({
pageNo: 1,
isLoading: false,
items: []
});
const update = useCallback(() => {
setState(state => ({...state, isLoading: true}));
someApi(state.pageNo);
setState(state => ({
...state,
pageNo: pageNo + 1
}));
setState(state => ({...state, isLoading: false}));
}, [isLoading, pageNo]);
const observerCallback = useCallback((entries, observer) => {
for (const entry of entries) {
if (entry.isIntersecting) {
observer.disconnect();
update();
}
}
}, [update]);
const observer = useRef(new IntersectionObserver(observerCallback)); // The callback is the function binding the update function that binds some of the initial state
const lastEl = useRef(null);
const preLastEl = useRef(null);
useEffect(() => {
update();
}, [props]);
if (lastEl.current && lastEl.current != preLastEl.current) {
preLastEl.current = lastEl.current;
observer.observe(lastEl.current);
}
return (
<SomeProgressBar style={{ display: state.isLoading ? "" : "none" }}/>
{
state.items.map((item) => <B ... ref={lastEl}/>)
}
);
}
I don't exactly why you're using the ref and why you can't do it differently. so in case you have to do it this way, your refs are dependent to state object and they need to be changed when the state are changed so you should use a useEffect to change the refs based on new state. try to implement one of these two steps:
1
const refs = useRef({
lastEl: undefined,
observer: new IntersectionObserver((entries, observer) => {
...
update(state.pageNo); // This is the update function bound when the instance of this component gets initialized
});
});
useEffect(() => {
update(state.pageNo);
}, [props]);
function update(pageNo = 1) {
setState(prev => ({...prev, isLoading: true}));
someApi(pageNo); // state.pageNo will be always 1
setState(prev => ({...prev, isLoading: false}));
}
2 in case above code didn't work try this
useEffect(() => {
if(state.pageNo){
refs.current = {
lastEl: undefined,
observer: new IntersectionObserver((entries, observer) => {
...
update(state.pageNo); // This is the update function bound when the instance of this component gets initialized
});
}
}
}, [state.pageNo])
// EditableNote.js
function EditableNote({ note }) {
const [editableNote, setEditableNote] = useState(note);
const { title, content } = editableNote;
const dispatch = useDispatch();
useEffect(() => {
setEditableNote(note);
}, [note]);
›
useEffect(() => {
dispatch(saveEditableNote(editableNote)); // I think here is problem
}, [dispatch, editableNote]);
const handleBlur = e => {
const name = e.target.id;
const value = e.currentTarget.textContent;
setEditableNote({ ...editableNote, [name]: value });
};
return (
<EditNote spellCheck="true">
<NoteTitle
id="title"
placeholder="Title"
onBlur={handleBlur}
contentEditable
suppressContentEditableWarning={true}>
{title}
</NoteTitle>
<NoteContent
id="content"
placeholder="Note"
onBlur={handleBlur}
contentEditable
suppressContentEditableWarning={true}>
{content}
</NoteContent>
</EditNote>
);
}
export default EditableNote;
I have EditableNote component which is contentEditable. I set its initial state through props from its parent(Note). So if something is changed in note, then editableNote has to changed.
To keep recent props state, I use useEffect. Everything seems working well.
Here is an issue. If I first change color of note and typing, it is updated as expected. But on contrast, if I first typing and change color, editableNote state is not updated.
// Reducer.js
case actions.GET_NOTE_COLOR:
return {
...state,
bgColor: action.payload
}
case actions.CHANGE_NOTE_COLOR:
return {
...state,
notes: state.notes.map(note => note.id === action.payload ?
{ ...note, bgColor: state.bgColor }
: note
)
};
case actions.SAVE_EDITABLE_NOTE: // payload is old value
return {
...state,
editableNote: action.payload,
}
I check what happened in an action. I found everything works until CHANGE_NOTE_COLOR but when dispatch SAVE_EDITABLE_NOTE, its payload is not updated!
I have no idea.. plz.. help me...TT
You have to use the connect wrapper provided by redux to connect actions and state of redux to your components
https://react-redux.js.org/api/connect
I want to use useEffect() to detect changes to a state value and pass that value to a parent component using a callback function received as a prop. I can't figure out a way to do this without disabling the eslint missing dependency warning. I have this problem on both the child component and that child's child.
Here is the parent implementation:
const updateValues = (newValues) => {
setValues({ ...values, ...newValues });
};
<GeneralUpdates onUpdate={updateValues} />
Here is the first child (GeneralUpdates):
const [values, setValues] = useState({
name: '',
description: '',
});
// This handles form input changes
const handleChange = (prop) => (event) => {
setValues({ ...values, [prop]: event.target.value });
};
useEffect(() => {
onUpdate(values);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [values]);
<FilesUpload handleChange={onUpdate}/>
And this is the child's child (FilesUpload):
const [featuredPhotos, setFeaturedPhotos] = useState([]);
useEffect(() => {
handleChange({ featuredPhotos });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [featuredPhotos]);
Adding handleChange as a dependency results in an infinite re-rendering loop. I've tried every solution I can find but must be missing something here.
A colleague helped me find a solution to this—passing a named function to useEffect rather than the anonymous one:
const updateCallback = () => {
onUpdate(values);
};
useEffect(updateCallback, [values]);
There isn't any problem with disabling eslint for this particular rule if you know 100% what you are doing.
However you can still get around this without disabling eslint by using useCallback hook for handleChange function being passed from parent if parent is a functional component
// in parent
const handleChange = useCallback(({featuredPhotos}) => {
// Do what you want to do here
}, []);
If parent is not a functional component, make sure you aren't using arrow function in render while passing it as props and you should be ok
handleChange = ({featuredPhotos}) => {
...
}
render() {
return (
<Child handleChange={this.handleChange} />
)
}
EDIT:
for your case, you can update the implementation by using useCallback for updateValue function and inside it use functional setState
const updateValues = useCallback((newValues) => {
setValues(prevValues => ({ ...prevValues, ...newValues }));
}, []);
I'm adding one more solution which keeps also eslint happy:
const callback = useRef();
callback.current = onUpdate;
useEffect(() => {
callback.current(values);
}, [values, callback]);
I created a custom hook to store Objects in a useState hook and allow changing properties without loosing the other entries.
const useObject = initialValue => {
const [state, setState] = useState(initialValue);
return [
state,
newState => {
setState({
...state,
...newState
});
}
];
};
This hook works in my component but doesn't when I assign it to my context.
Here is what I did:
I created a context:
export const navigation = createContext();
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/store.js:40-83
I created a useObject variable and assigned it as value to my Context Provider
<navigation.Provider value={useObject()}>
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/Layout.js:234-284
I load the context via useContext and change its value
const [navigationState, setNavigationState] = useContext(navigation);
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/App.js:476-616
Result:
The context always stores the new entry and removes all existing entries.
Anyone knows why ?
Here is the Sandbox link. You can test it by clicking the filter button. I expected to see {search:true, icon: 'times'} as context value. Thx!
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/App.js
There is one important things to note here. useEffect in App.js is run once and hence the onClick function set with setNavigationState will use the values from its closure at the point at which it is defined i.e initial render.
Due to this, when you call the function within Header.js from context's the value along with the localState are being reset to the initial value.
SOLUTION 1:
One solution here is to use callback approach to state update. For that you need to modify your implementation on useObject a bit to provide the use the capability to use the callback value from setState
const useObject = initialValue => {
const [state, setState] = useState(initialValue);
return [
state,
newState => {
if(typeof newState === 'function') {
setState((prev) => ({ ...prev, ...newState(prev)}));
} else {
setState({
...state,
...newState
});
}
}
];
};
and then use it in onContextClick function like
const onContextClick = () => {
setState(prevState => {
setNavigationState(prev => ({ icon: ICON[prevState.isOpen ? 0 : 1] }));
return { isOpen: !prevState.isOpen };
});
};
Working DEMO
SOLUTION 2:
The other simpler approach to solving the problem is to use useCallback for onContextClick and update the navigation state with useEffect, everytime the closure state is updated like
const onContextClick = React.useCallback(() => {
setNavigationState({ icon: ICON[state.isOpen ? 0 : 1] });
setState({ isOpen: !state.isOpen });
}, [state]);
useEffect(() => {
setNavigationState({
search: true,
icon: ICON[0],
onClick: onContextClick
});
}, [onContextClick]);
Working demo
I was trying to implement a simple paint component with React hooks. My expected behavior was 'mouseMove' to be executed when I moved my mouse while remaining clicked. However, state.isMouseDown always returned false within mouseMove().
Any fixes or references to potentially helpful documents would be grateful.
const initialState = {
isMouseDown: false,
isMouseMoving: false
};
const DrawingCanvas = () => {
const [state, setState] = useState(initialState);
useEffect(() => {
console.log('mounted');
document.addEventListener('mousedown', () => mouseDown());
document.addEventListener('mousemove', () => mouseMove());
}, []);
const mouseDown = () => {
console.log('mousedown');
setState(state => ({
...state,
isMouseDown: true
}));
};
const mouseMove = () => {
// why is this false even when click and move?
console.log('mouseMove:isMouseDown', state.isMouseDown);
if (!state.isMouseDown) return;
console.log('mousemove'); // this line is not being executed
setState(state => ({
...state,
isMouseMoving: true
}));
};
console.log(state);
return (
<div>
<p>mouseDown: {`${state.isMouseDown}`}</p>
<p>mouseMoving: {`${state.isMouseMoving}`}</p>
</div>
);
};
As explained in this related answer, the problem is that event listener accesses state object from the scope where it was defined, i.e. initial state, because event is listened on component mount.
A solution is to either use mutable state, or access state exclusively from state updater function. In the code above, state.isMouseDown refers to original state. In case it's needed to avoid state updates, state updater can return original state:
const mouseMove = () => {
setState(state => {
if (!state.isMouseDown)
return state; // skip state update
else
return {
...state,
isMouseMoving: true
};
});
};