React Hooks: Can't access updated useState variable - reactjs

Issue: inputVal is a state variable that I update using setState.
It works fine when the button calls my handleConfirmSubmit function.
However, if I add that eventListener that listens for the keypress then triggers the function onKeyUp which then triggers handleConfirmSubmit... all of a sudden handleConfirmSubmit cannot see the updated state value.
I added the useEffect to console log out inputVal as I type, and I can confirm that it's updating, but for some reason, depending on where I invoke handleConfirmSubmit it either can or cannot see the updated state value.
CODE:
(Some unnecessary stuff has been stripped out)
export default function SingleInputModal(props){
const [cancelButton, setCancelButton] = useState(null);
const [inputVal, setInputVal] = useState(props.previousValue || '');
//ComponentDidMount
useEffect( () => {
//Listen for keypress
window.addEventListener('keyup', onKeyUp);
return () => {
window.removeEventListener('keyup', onKeyUp);
}
}, []);
useEffect( () => {
console.log('new inputval', inputVal);
},[inputVal])
const handleChange = (e) => {
console.log('handle Change ran', e.target.value);
setInputVal(e.target.value || '');
}
const handleConfirmSubmit = () => {
console.log('About to submit: ', inputVal);
props.handleConfirmation(false, inputVal)
}
const onKeyUp = (e) => {
if(props.submitOnEnter && e.key === 'Enter'){
//Submit
handleConfirmSubmit();
}
if(props.cancelOnESC && e.key === 'Escape'){
//Cancel
props.handleConfirmation(true, null);
}
}
return(
<div className="confirm"
>
<div className="modal singleInput"
>
<div className="content">
<input
className='myModal__textInput'
type='text'
value={inputVal}
onChange={handleChange}
/>
</div>
<div className="actions">
<a
onClick={handleConfirmSubmit}
>Submit</a>
</div>
</div>
</div>
)
}

You can update your listener when inputVal changes, otherwise the listener will store a copy of the value of inputVal from the first render.
useEffect( () => {
//Listen for keypress
window.addEventListener('keyup', onKeyUp);
return () => {
window.removeEventListener('keyup', onKeyUp);
}
}, [inputVal]);

You could use React.useCallback to make sure you have fresh versions of the functions (since e.g. handleConfirmSubmit can capture the inputVal in its closure):
export default function SingleInputModal(props) {
const [cancelButton, setCancelButton] = useState(null);
const [inputVal, setInputVal] = useState(props.previousValue || "");
useEffect(() => {
window.addEventListener("keyup", onKeyUp);
return () => window.removeEventListener("keyup", onKeyUp);
}, []);
useEffect(() => console.log("new inputval", inputVal), [inputVal]);
const handleChange = e => {
console.log("handle Change ran", e.target.value);
setInputVal(e.target.value || "");
};
const handleConfirmSubmit = React.useCallback(
() => {
console.log("About to submit: ", inputVal);
props.handleConfirmation(false, inputVal);
},
[inputVal],
);
const onKeyUp = React.useCallback(
e => {
if (props.submitOnEnter && e.key === "Enter") {
// Submit
return handleConfirmSubmit();
}
if (props.cancelOnESC && e.key === "Escape") {
// Cancel
props.handleConfirmation(true, null);
}
},
[handleConfirmSubmit, props.submitOnEnter, props.cancelOnESC],
);
return (
<div className="confirm">
<div className="modal singleInput">
<div className="content">
<input className="myModal__textInput" type="text" value={inputVal} onChange={handleChange} />
</div>
<div className="actions">
<a onClick={handleConfirmSubmit}>Submit</a>
</div>
</div>
</div>
);
}

Related

clickOutside hook doesn't work on 2nd click

I have a list of items and want to include an icon that opens a modal for a user to choose 'edit' or 'delete' the item.
And I put this code inside the ActionModal so that only clicked modal would open by comparing the ids.
The problem is, clicking outside the element work only one time and after that, nothing happens when the ellipsis button clicked. I think it's probably because the state inside ActionModal, 'modalOpen' remains false, but I'm stuck here and don't know how to handle it.
if (!isOpen.show || isOpen.id !== id || !modalOpen) return null;
const List = () => {
const [modal, setModal] = useState({ id: null, show: false });
const onDialogClick = (e) => {
setModal((prevState) => {
return { id: e.target.id, show: !prevState.show };
});
};
const journals = journals.map((journal) => (
<StyledList key={journal.id}>
<Option>
<FontAwesomeIcon
icon={faEllipsisV}
id={journal.id}
onClick={onDialogClick}
/>
<ActionModal
actions={['edit', 'delete']}
id={journal.id}
isOpen={modal}
></ActionModal>
</Option>
const ActionModal = ({ id, actions, isOpen }) => {
const content = actions.map((action) => <li key={action}>{action}</li>);
const ref = useRef();
const [modalOpen, setModalOpen] = useState(true);
useOnClickOutside(ref, () => setModalOpen(!modalOpen));
if (!isOpen.show || isOpen.id !== id || !modalOpen) return null;
return (
<StyledDiv>
<ul ref={ref}>{content}</ul>
</StyledDiv>
);
};
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
So fistly you do not need to include ref and handler as dependencies in useEffect hook, because evvent listeners are set on initial load, there is no need to set it on every value change.
I think I don't fully understand your situation. So you need to close the modal after it is opened by pressing outside it? or you want to be able to press that 3 dots icon when it's opened?
P.S.
I little bit condesed your code. Try this and let me know what is happening. :)
const ActionModal = ({ id, actions, isOpen, setOpen }) => {
const ref = useRef();
const content = actions.map((action) => <li key={action}>{action}</li>);
useEffect(() => {
const listener = (event) => {
if (ref.current || !ref.current.contains(event.target)) {
setOpen({...open, show: false});
}
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, []);
return (
<StyledDiv>
<div ref={ref}>
<ul>{content}</ul>
</div>
</StyledDiv>
);
};

Fetch data before component render, Reactjs

I am having a slight issue with my react code. I want the data to be completely fetched before rendering. However, I have tried various ways such as 'setGroupLoaded to true' after the async call, but it is still not working. When the component first mounts, 'groupLoaded == false and myGroup == [],', then it goes to the next conditional statement where 'groupLoaded == true' but the myGroup [] is still empty. I was expecting the myGroup [] to be filled with data since groupLoaded is true. Please I need help with it.
function CreateGroup({ currentUser }) {
const [name, setName] = useState("");
const [myGroup, setMyGroup] = useState([]);
const [groupAdded, setGroupAdded] = useState(false);
const [groupLoaded, setGroupLoaded] = useState(false);
const handleChange = (e) => {
const { value, name } = e.target;
setName({
[name]: value,
});
};
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
useEffect(() => {
let groupId = myGroup.length ? myGroup[0].id : "";
let groupName = myGroup.length ? myGroup[0].groupName : "";
if (myGroup.length) {
JoinGroup(currentUser, groupId, groupName);
setTimeout(() => fetchGroupMembers(), 3000);
}
}, [myGroup]);
let itemsToRender;
if (groupLoaded && myGroup.length) {
itemsToRender = myGroup.map((item) => {
return <Group key={item.id} item={item} deleteGroup={deleteGroup} />;
});
} else if (groupLoaded && myGroup.length === 0) {
itemsToRender = (
<div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="exampleInputTitle">Group Name</label>
<input
type="text"
className="form-control"
name="name"
id="name"
aria-describedby="TitleHelp"
onChange={handleChange}
/>
</div>
<button type="submit" className="btn btn-primary">
Add group{" "}
</button>
</form>
</div>
);
}
return (
<div>
<h3>{currentUser ? currentUser.displayName : ""}</h3>
{itemsToRender}
</div>
);
}
The problem is here:
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
When you call setGroupLoaded(true), the first call to setMyGroup(groupArr) hasn't happened yet because fetchMyGroup(currentUser) is asynchronous. If you've never done this before, I highly recommend putting in some logging statements, to see the order in which is executes:
useEffect(() => {
const fetchGroupCreated = async () => {
console.log("Got data")
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
console.log("Before starting to load")
fetchGroupCreated();
setGroupLoaded(true);
console.log("After starting to load")
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
The output will be:
Before starting to load
After starting to load
Got data
This is probably not the order you expected, but explains exactly why you get groupLoaded == true, but the myGroup is still empty.
The solution (as Nick commented) is to move the setGroupLoaded(true) into the callback, so that it runs after the data is retrieved:
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
setGroupLoaded(true);
};
fetchGroupCreated();
An alternative approach may be to await the call to fetchGroupCreated(). I'm not sure if it'll work, but if it does it'll be simpler:
await fetchGroupCreated();

How to update an array of state using useEffect

I want to get latest state after updating state.
So I need to use useEffect.
After I change content of todo, I call saveEditedTodo onBlur.
So my code is,
useEffect(() => {
console.log(todos)
// I need to setTodos(todos), but it causes infinite loop
}, [todos]);
const saveEditedTodo = (e, id) => {
const newContent = e.currentTarget.innerHTML;
const editedTodo = todos.map((todo) =>
todo.id === id ? { ...todo, todoItem: newContent } : todo,
);
setTodos(editedTodo); // Re-rendering
onBlur(todos); // Re-rendering
};
And onBlur from props is,
const handleOnBlurTodo = (value) => {
const newValue = convertTodoToNote(value);
setEditableNote({ ...editableNote, content: newValue });
};
How can I get latest state using useEffect?
(+) Here is my full code!
function TodoList({ todoContent, onBlur }) {
const [todos, setTodos] = useState(todoContent);
const [isHover, setIsHover] = useState({ hoverID: '', onHover: false });
const { hoverID, onHover } = isHover;
const isEditable = useSelector((state) => state.isSelected);
const doneTodo = todos ? todos.filter((todo) => todo.isDone).length : 0;
useEffect(() => {
console.log(todos);
}, [todos]);
const saveEditedTodo = (e, id) => {
const newContent = e.currentTarget.innerHTML;
const editedTodo = todos.map((todo) =>
todo.id === id ? { ...todo, todoItem: newContent } : todo,
);
setTodos(editedTodo); // Re-rendering
onBlur(todos); // Re-rendering
};
const handleDeleteTodo = (id) => {
let newTodos = todos.filter((el) => el.id !== id);
setTodos(newTodos);
onBlur(todos);
};
const handleOnMouseOver = (id) => {
setIsHover({ hoverID: id, onHover: true });
};
const handleOnMouseLeave = (id) => {
setIsHover({ hoverID: id, onHover: false });
};
const handleCheckbox = (id) => {
const newTodos = todos.map((todo) =>
todo.id === id ? { ...todo, isDone: !todo.isDone } : todo,
);
setTodos(newTodos);
console.log('[todos]' + todos);
};
const todoTask = todos.filter((todo) => !todo.isDone);
const doneTask = todos.filter((todo) => todo.isDone);
if (isEditable && todos) {
let todoList = todoTask.map((todo, i) => (
<TodoListContainer
key={i}
onMouseEnter={() => handleOnMouseOver(todo.id)}
onMouseLeave={() => handleOnMouseLeave(todo.id)}
>
<Checkbox
type="checkbox"
checked={todo.isDone}
onChange={() => handleCheckbox(todo.id)}
/>
<NoteTitle
isTodoItem
size="medium"
placeholder="Add Todo"
onBlur={(e) => saveEditedTodo(e, todo.id)}
contentEditable
suppressContentEditableWarning="true"
>
{todo.todoItem}
</NoteTitle>
{hoverID === todo.id && onHover && (
<Tool
title="Delete Todo"
bgImage={DeleteIcon}
deleteTodo={() => handleDeleteTodo(todo.id)}
/>
)}
</TodoListContainer>
));
let doneList = doneTask.map((todo, i) => (
<TodoListContainer
key={i}
onMouseEnter={() => handleOnMouseOver(todo.id)}
onMouseLeave={() => handleOnMouseLeave(todo.id)}
>
<Checkbox
type="checkbox"
onBlur={() => handleCheckbox(todo.id)}
checked={todo.isDone}
/>
<NoteTitle
isTodoItem
size="medium"
placeholder="Add Todo"
onInput={(e) => saveEditedTodo(e, todo.id)}
contentEditable
suppressContentEditableWarning="true"
>
{todo.todoItem}
</NoteTitle>
{hoverID === todo.id && onHover && (
<Tool
title="Delete Todo"
bgImage={DeleteIcon}
deleteTodo={() => handleDeleteTodo(todo.id)}
/>
)}
</TodoListContainer>
));
return (todoList = (
<div>
{todoList}
{doneTodo > 0 && <CompletedTodo doneTodo={doneTodo} />}
{doneList}
</div>
));
}
if (!isEditable && todos) {
const todoList = todos.map((todo, i) => (
<TodoListContainer key={i}>
<Checkbox
type="checkbox"
onChange={() => handleCheckbox(todo.id)}
checked={todo.isDone}
/>
<NoteTitle size="small">{todo.todoItem}</NoteTitle>
</TodoListContainer>
));
return todoList;
}
return null;
}
export default TodoList;
Generally React.useEffect() is used for performing side effects for a React component. What I believe is that you wish to get the new state rendered on screen after saving the TODO content, and that can be just achieved by an onChange handler wherever you are receiving the input for your todos.
<TextField onChange={(e) => saveEditedTodos(e, id)} />
This will trigger the saveEditedTodos callback every time the value of the TextField changes. If you want to trigger the callback on clicking a save button, you can add an onClick handler in the Button component.
Another scenario what I can imagine is that you're saving your TODOs somewhere, so you want to update the list on the screen after saving the TODO in some storage, in that case you can fetch the value of todoList on each save. This can be done inside a useEffect hook callback.
React.useEffect(() => {
fetchTodos().then((response) => setTodos(response.data))
})
Here fetchTodos() is a JS Promise or async function which fetches the updated state of TODOs and sets the received data using setTodos

ReactJS - Autosave with multiple inputs - best practices

The goal I want to achieve is to implement the autosave function without hurting the performance (useless rerenders etc).
Ideally, when the autosave will happen, the state will also be updated.
I created an example component with 3 inputs, in this example the component rerenders on every keystroke. I also have a useEffect hook in which I'm looking for data changes and then I save them after 1sec.
The ChildComponent is used to preview the input data.
function App(props) {
const timer = React.useRef(null);
const [data, setData] = React.useState(props.inputData);
React.useEffect(() => {
clearTimeout(timer.current)
timer.current = setTimeout(() => {
console.log("Saving call...", data)
}, 1000)
}, [data])
const inputChangeHandler = (e, type) => {
if (type === "first") {
setData({ ...data, first: e.target.value })
} else if (type === "second") {
setData({ ...data, second: e.target.value })
} else if (type === "third") {
setData({ ...data, third: e.target.value })
}
}
return (
<>
<div className="inputFields">
<input
defaultValue={data.first}
type="text"
onChange={(e) => inputChangeHandler(e, "first")}
/>
<input
defaultValue={data.second}
type="text"
onChange={(e) => inputChangeHandler(e, "second")}
/>
<input
defaultValue={data.third}
type="text"
onChange={(e) => inputChangeHandler(e, "third")}
/>
</div>
<ChildComponent data={data} />
</>
)
}
I've read about debounce but my implementation didn't work. Has anyone run into the same problem?
Below is my debounce implementation using lodash:
React.useEffect(() => {
console.log("Saving call...", data)
}, [data])
const delayedSave = React.useCallback(_.debounce(value => setData(value), 1000), []);
const inputChangeHandler = (e, type) => {
if (type === "first") {
let obj = { ...data };
obj.first = e.target.value;
delayedSave(obj)
} else if (type === "second") {
let obj = { ...data };
obj.second = e.target.value;
delayedSave(obj)
} else if (type === "third") {
let obj = { ...data };
obj.third = e.target.value;
delayedSave(obj)
}
}
The problem with this one is that if a user types immediately (before the 1sec delay) from the first input to the second it only saves the last user input.
The problem in your implementation is that the timer is set in a closure (in useEffect) using the data your component had before the timer starts. You should start the timer once the data (or the newData in my implementation proposal) is changed. Something like:
function App(props) {
const [data, setData] = React.useState(props.inputData);
const { current } = React.useRef({ data, timer: null });
const inputChangeHandler = (e, type) => {
current.data = { ...current.data, [type]: e.target.value };
if(current.timer) clearTimeout(current.timer);
current.timer = setTimeout(() => {
current.timer = null;
setData(current.data);
console.log("Saving...", current.data);
}, 1000);
}
return (
<>
<input defaultValue={data.first} type="text" onChange={(e) => inputChangeHandler(e, "first")} />
<input defaultValue={data.second} type="text" onChange={(e) => inputChangeHandler(e, "second")} />
<input defaultValue={data.third} type="text" onChange={(e) => inputChangeHandler(e, "third")} />
</>
);
}
You should set the timer id to null inside the timer handler.
React.useEffect(()=>{
clearTimeout(timer.current)
timer.current = setTimeout(()=>{
timer.current = null;
console.log("Saving...",data)
},1000)
},[data])

setInterval does not work properly in ReactJS

I got a problem with setInterval in ReactJS (particularly Functional Component). I want to enable autoplay slider (it means value in Slider Bar will be increased after a period of time). This function will be triggered when I click a button. But I don't know exactly how setInterval works with setValue. The console log just returns the initial value (does not change) (console.log function was called in a callback function of setInterval).
My code is below:
const [value, setValue] = useState(10);
const [playable, setPlayable] = useState(false);
useEffect(() => {
if (playable === true) {
console.log("Trigger");
const intervalId = setInterval(increaseValue, 1000);
return () => clearInterval(intervalId);
}
}, [playable]);
const increaseValue = () => {
console.log(value);
setValue(value + 1);
};
const autoPlay = () => {
setPlayable(true);
};
return (
<div>
<div style={{"width": "800px"}}>
<Slider
value={value}
step={1}
valueLabelDisplay="on"
getAriaValueText={valueText}
marks={marks}
onChange={handleChange}
min={0}
max={100}
/>
</div>
<button onClick={autoPlay}>Play</button>
</div>
)
So, I think you need to pass increaseValue function into useEffect
useEffect(() => {
if (playable === true) {
console.log("Trigger");
const intervalId = setInterval(increaseValue, 1000);
return () => clearInterval(intervalId);
}
}, [playable, increaseValue]);

Resources