I have the following code and it is working fine. <AddContact /> is a simple component that presents a input form which collects name + email from user - I have attached its code at the end for completeness. The collected contacts array is stored in localStorage, and when I refresh the page, they simply get reloaded. all good
import './App.css'
import { useState, useEffect } from 'react'
function App() {
const LOCAL_STORAGE_KEY = 'contacts'
const [contacts, setContacts] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [])
const addNewContact = (newContact) => {
setContacts([...contacts, newContact])
}
useEffect(() => {
console.table(contacts)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}, [contacts])
return (
<AddContact newContact={addNewContact} />
)
}
export default App
my question is that the following revision does not work - every time the page is refreshed, local storage is wiped out. But it really look like it should work - I was following an online tutorial and it was working when the instructor did it.
import './App.css'
import { useState, useEffect } from 'react'
function App() {
const LOCAL_STORAGE_KEY = 'contacts'
const [contacts, setContacts] = useState([]) // changed line
const addNewContact = (newContact) => {
setContacts([...contacts, newContact])
}
useEffect(() => {
console.table(contacts)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}, [contacts])
// added
useEffect(() => {
const savedContacts = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_KEY)
)
if (savedContacts) {
setContacts(savedContacts)
}
}, [])
return (
<AddContact newContact={addNewContact} />
)
}
export default App
for completeness, here's the code for <AppContact />
import React, { Component } from 'react'
export class AddContact extends Component {
state = {
name: '',
email: '',
}
updateState = (e) => {
this.setState({ [e.target.name]: e.target.value })
}
addContact = (e) => {
e.preventDefault()
if (this.state.name === '' || this.state.email === '') {
return
}
this.props.newContact(this.state)
}
render() {
return (
<div className='ui main'>
<h2>Add Contact</h2>
<form className='ui form' onSubmit={this.addContact}>
<div className='field'>
<label>Name</label>
<input
type='text'
name='name'
value={this.state.name}
placeholder='Name'
onChange={this.updateState}
/>
</div>
<div className='field'>
<label>Email</label>
<input
type='text'
name='email'
value={this.state.email}
placeholder='Email'
onChange={this.updateState}
/>
</div>
<button className='ui button blue' type='submit'>
Add
</button>
</form>
</div>
)
}
}
export default AddContact
I would like to understand why the second method does not work.
The value you set when calling addNewContact does get stored in localStorage when the first useEffect runs (as expected). The problem is that, when you reload the page, that same useEffect is overwriting what's in localStorage because the state is reset to an empty array ([]). This triggers the useEffect with contacts equals to [], and stores that in localStorage.
There are a few ways to handle it, but one way is to check if contacts is an empty array before storing its value to localStorage.
useEffect(() => {
console.table(contacts)
if (contacts.length > 0) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}
}, [contacts])
This prevents the initial state of contacts to be stored in localStorage when the page gets first loaded.
In a given render, effect callbacks will run in the order that they're declared. With this:
useEffect(() => {
console.table(contacts)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}, [contacts])
// added
useEffect(() => {
const savedContacts = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_KEY)
)
if (savedContacts) {
setContacts(savedContacts)
}
}, [])
On mount, the first one runs before the second - you call localStorage.setItem before the second one runs localStorage.getItem - so by the time the second one runs, storage has been set to the initial value of the contacts state, which is the empty array.
To fix it, reverse their order, so that the one that calls .getItem runs first.
useEffect(() => {
const savedContacts = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_KEY)
)
if (savedContacts) {
setContacts(savedContacts)
}
}, []);
useEffect(() => {
console.table(contacts)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}, [contacts]);
That said, your first approach of
const [contacts, setContacts] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [])
looks a lot nicer than an effect hook with an empty dependency array, IMO.
Related
I am new to react and creating my first react app. not sure why the todo list is not saved even though I have used localStorage set and get methods. I am also getting error about the key in my map method. I can't seen to find any issues on my own with the code.Below is the code of the todo list App
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
setTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
setTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
setTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
import Todo from './Todo'
export default function TodoList({todo,toggleTodo}) {
return (
todo.map((todo)=> {
return <Todo key={todo.id} todo={todo} toggleTodo={toggleTodo} />
})
)
}
This:
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
Is probably taking the initial state of todos on the first render (empty array) and overwriting what data was in their with that initial state.
You might think the previous effect counters this since todos is populated from local storage -- but it doesn't, because on that initial render pass, the second effect will only see the old value of todos. This seems counter-intuitive at first. But it's because whenever you call a set state operation, it doesn't actual change the value of todos immediately, it waits until the render passes, and then it changes for the next render. I.e. it is, in a way, "queued".
For the local storage setItem, you probably want to do it in the event handler of what manipulates the todos and not in an effect. See the React docs.
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
const storeTodos = (todos) => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
setTodos(todos)
}
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
storeTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
storeTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
storeTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
As the for the key error, we'd need to see the code in TodoList, but you need to ensure when you map over them, that the id property of each todo is passed to a key prop on the top most element/component within the map callback.
I have a form where user can enter a name that will then be displayed on a list. Upon entering a new name the list should automatically be sorted in alphabetical order. Current attempt with useEffect does work but is buggy(list will only be sorted after user start deleting previous input text).
A few notable things to highlight with current setup:
Submission component is used for rendering list of names
Form component is used to store state of app and input fields
handleSortName() will execute sorting
useEffect() executes handleSortName() when there is a change to submissions value
import React, { useEffect, useState } from "react";
const Submission = ({ submission }) => {
return <div>name: {submission.name}</div>;
};
const Form = () => {
const [values, setValues] = useState({
name: ""
});
const [submissions, setSubmission] = useState([
{ name: "John" }
]);
const addSubmission = (values) => {
const newSubmissions = [...submissions, values];
setSubmission(newSubmissions);
};
const handleChange = (event) => {
const value = event.target.value;
setValues({ ...values, [event.target.name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
addSubmission(values);
handleSortName(submissions);
};
const handleSortName = (submissions) => {
return submissions.sort((a, b) => a.name.localeCompare(b.name));
};
useEffect(() => {
handleSortName(submissions);
}, [submissions]);
return (
<>
<form onSubmit={handleSubmit}>
<h1>Student Enrollment</h1>
<div>
<label>name: </label>
<input
required
type="text"
name="name"
value={values.name}
onChange={handleChange}
/>
<input type="submit" value="Submit" />
</div>
</form>
<h1>Submitted Student</h1>
{submissions.map((submission, index) => (
<Submission key={index} submission={submission} />
))}
</>
);
};
export default Form;
Working Sample: https://codesandbox.io/s/usestate-form-oj61v9?file=/src/Form.js
I am aware that useState is asynchronous and will not update value right away.
Any suggestion on other implementations such as functional updates, a custom hook or current UseEffect approach? Thanks in Advance!
UPDATE:
because React re-renders the component when the props or state changes. that means inside your handleSortName() function you have to call setSubmissions with the new sorted array, then React will know that the state was changed.
const handleSortName = (submissions) => {
// create a new copy of the array with the three dots operator:
let copyOfSubmissions = [...submissions];
// set the state to the new sorted array:
setSubmissions(
copyOfSubmissions.sort((a, b) => a.name.localeCompare(b.name))
);
};
or you can do both steps in 1 line:
const handleSortName = (submissions) => {
// set the state to the newly created sorted array with the three dots operator:
setSubmissions(
[...submissions].sort((a, b) => a.name.localeCompare(b.name))
);
};
sandbox link here
I am new to React, and I have to build a timeout mechanism for a page. I used react-idle-timer, with some help found on the Internet. However, when I try to access the page, I get a Minified React error #321, in which it tells me that I used hooks incorrectly.
Can you please take a look on the following code and point me in the right direction? Thanks
import React from "react"
import NavBar from "./Navbar"
import "../styles/Upload.css"
import LinearProgressWithLabel from "./LinearProgressWithLabel"
import axios from "axios"
import Logout from "./Logout"
import { useIdleTimer } from 'react-idle-timer'
import { format } from 'date-fns'
export default function Upload() {
const [selectedFile, setSelectedFile] = React.useState();
const [progress, setProgress] = React.useState(0);
const timeout = 3000;
const [remaining, setRemaining] = React.useState(timeout);
const [elapsed, setElapsed] = React.useState(0);
const [lastActive, setLastActive] = React.useState(+new Date());
const [isIdle, setIsIdle] = React.useState(false);
const handleOnActive = () => setIsIdle(false);
const handleOnIdle = () => setIsIdle(true);
const {
reset,
pause,
resume,
getRemainingTime,
getLastActiveTime,
getElapsedTime
} = useIdleTimer({
timeout,
onActive: handleOnActive,
onIdle: handleOnIdle
});
const handleReset = () => reset();
const handlePause = () => pause();
const handleResume = () => resume();
React.useEffect(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
setInterval(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
}, 1000)
}, []);
function changeHandler(event) {
setSelectedFile(event.target.files[0])
};
function handleSubmission() {
if (selectedFile) {
var reader = new FileReader()
reader.readAsArrayBuffer(selectedFile);
reader.onload = () => {
sendFileData(selectedFile.name, new Uint8Array(reader.result), 4096)
};
}
};
function sendFileData(name, data, chunkSize) {
function sendChunk(offset) {
var chunk = data.subarray(offset, offset + chunkSize) || ''
var opts = { method: 'POST', body: chunk }
var url = '/api/uploaddb?offset=' + offset + '&name=' + encodeURIComponent(name)
setProgress(offset / data.length * 100)
fetch(url, opts).then(() => {
if (chunk.length > 0) {
sendChunk(offset + chunk.length)
}
else {
axios.post('/api/uploaddb/done', { name })
.then(setProgress(100))
.catch(e => console.log(e));
}
})
}
sendChunk(0);
};
return (
<div>
<NavBar />
<div>
<div>
<h1>Timeout: {timeout}ms</h1>
<h1>Time Remaining: {remaining}</h1>
<h1>Time Elapsed: {elapsed}</h1>
<h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
<h1>Idle: {isIdle.toString()}</h1>
</div>
<div>
<button onClick={handleReset}>RESET</button>
<button onClick={handlePause}>PAUSE</button>
<button onClick={handleResume}>RESUME</button>
</div>
</div>
<h1>Upload</h1>
<input type="file" name="file" onChange={changeHandler} />
{!selectedFile ? <p className="upload--progressBar">Select a file</p> : <LinearProgressWithLabel className="upload--progressBar" variant="determinate" value={progress} />}
<br />
<div>
<button disabled={!selectedFile} onClick={handleSubmission}>Submit</button>
</div>
</div>
)
}
Well, in this case, you should avoid setting states inside the useEffect function, because this causes an infinite loop. Everytime you set a state value, your component is meant to render again, so if you put states setters inside a useEffect function it will cause an infinite loop, because useEffect function executes once before rendering component.
As an alternative you can set your states values outside your useEffect and then put your states inside the useEffect array param. The states inside this array will be "listened" by useEffect, when these states change, useEffect triggers.
Something like this:
React.useEffect(() => {
}, [state1, state2, state3]);
state anti-pattern
You are using a state anti-pattern. Read about Single Source Of Truth in the React Docs.
react-idle-timer provides getRemainingTime, getLastActiveTime and getElapsedTime
They should not be copied to the state of your component
They are not functions
getRemainingTime(), getLastActiveTime(), or getElapsedTime() are incorrect
To fix each:
getRemainingTime should not be stored in state of its own
Remove const [remaining, setRemaining] = useState(timeout)
Remove setRemaining(getRemainingTime) both places in useEffect
Change <h1>Time Remaining: {remaining}</h1>
To <h1>Time Remaining: {getRemainingTime}</h1>
The same is true for lastActive.
getLastActive should be be stored in state of its own
Remove const [lastActive, setLastActive] = React.useState(+new Date())
Remove setLastActive(getLastActiveTime()) both places in useEffect
Change <h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
To <h1>Last Active: {format(getLastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
And the same is true for elapsed.
getElapsedTime should be be stored in state of its own
Remove const [elapsed, setElapsed] = React.useState(+new Date())
Remove setElapsed(getElapsedTime()) both places in useEffect
Change <h1>Time Elapsed: {elapsed}</h1>
To <h1>Time Elapsed: {getElapsedTime}</h1>
remove useEffect
Now your useEffect is empty and it can be removed entirely.
unnecessary function wrappers
useIdleTimer provides reset, pause, and resume. You do not need to redefine what is already defined. This is similar to the anti-pattern above.
Remove const handleReset = () => reset()
Change <button onClick={handleReset}>RESET</button>
To <button onClick={reset}>RESET</button>
Remove const handlePause = () => pause()
Change <button onClick={handlePause}>PAUSE</button>
To <button onClick={pause}>PAUSE</button>
Remove const handleResume = () => resume()
Change <button onClick={handleResume}>RESUME</button>
To <button onClick={resume}>RESUME</button>
avoid local state
timeout should be declared as a prop of the Upload component
Remove const timeout = 3000
Change function Upload() ...
To function Upload({ timeout = 3000 }) ...
To change timeout, you can pass a prop to the component
<Upload timeout={5000} />
<Upload timeout={10000} />
use the provided example
Read Hook Usage in the react-idle-timer docs. Start there and work your way up.
import React from 'react'
import { useIdleTimer } from 'react-idle-timer'
import App from './App'
export default function (props) {
const handleOnIdle = event => {
console.log('user is idle', event)
console.log('last active', getLastActiveTime())
}
const handleOnActive = event => {
console.log('user is active', event)
console.log('time remaining', getRemainingTime())
}
const handleOnAction = event => {
console.log('user did something', event)
}
const { getRemainingTime, getLastActiveTime } = useIdleTimer({
timeout: 1000 * 60 * 15,
onIdle: handleOnIdle,
onActive: handleOnActive,
onAction: handleOnAction,
debounce: 500
})
return (
<div>
{/* your app here */}
</div>
)
}
Here is the where I am having the problem,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
Here when I am trying to log the 'parsedId' it logs the data correctly
ioNng23DkIM
And after using the setVideoId() function when I try to log the value it returns undefined
undefined
Here is a snap shot of the log output.
Home.js code:
import React, { useRef, useState } from "react";
import { Link } from "react-router-dom";
import getYouTubeID from 'get-youtube-id';
function Home(props) {
const [videoLink, setVideoLink] = useState();
const [isBool, setBool] = useState(false);
const [videoId, setVideoId] = useState();
const urlRef = useRef();
const handleChange = (event) => {
setVideoLink(event.target.value);
if (urlRef.current.value === '') {
alert('Please enter a URL');
setBool(true);
} else {
setBool(false);
}
}
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
return (
<section className="homeLayout">
<div className="logo-display">
<img className="logo-img" alt="logo" src="./logo.png" />
<h1>WatchIt</h1>
</div>
<div className="searchlayer">
<form>
<input ref={urlRef} id="videoLink" placeholder="Enter the youtube video URL:" onBlur={handleChange} required />
<Link style={{ pointerEvents: isBool ? 'none' : 'initial' }} to={`/play?=${videoId}`} onClick={handleCLick}>Play</Link>
</form>
</div>
</section>
);
}
export default Home;
You can use useEffect to solve your problem.
Use effect will listen to you state change n then you can perform logic in there.
The problem you're facing is because setState will set the value eventually, not immediately (Usually this means the update will be visible when the component is rendered again). If you want to do something after the value is set, you need to use useEffect.
Splitting your handleClick we get,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId); // Queue the change for `videoId`
}
useEffect(() => {
console.log(videoId);
}, [videoId]); // Call this function when the value of `videoId` changes
I try to implement the Search function into my management system using React-Hooks and GraphQL-Apollo Client. While the interface is shown successfully, when I press the 'Search' button it came out an error which named:
Invalid hook call. Hooks can only be called inside of the body of a function component.
I'm pretty sure the useQuery is being called inside a function, so I do not understand what will cause this. The other function such as display all users and add new users are working fine.
I have tried a couple of ways to implement the search function and search around online while still couldn't get it solve. This is my first time encounters React-Hooks too.
Here is my current code in the searchUser component
import React from 'react'
import {useQuery} from 'react-apollo-hooks';
import {searchUserQ} from '../queries/queries';
const SearchUserForm = () => {
let name = '';
let content;
return (
<div id="edit-user">
<div className="field">
<label htmlFor="name">Search UserName</label>
<input type="text" id="name" onChange={ (event) => {
name = event.target.value }}/>
<button onClick={(event) => {
event.preventDefault();
const { data, loading, error } = useQuery(searchUserQ, {
variables: { name: name },
suspend: false
});
if (loading) {
content = <p>Loading User...</p>
}
if (error){
console.log(`Error Occur: ${ error }`);
content = <p>Error Occur!</p>
}
content = data.users.map(user => (
<p key={user.id}>Username: {user.name} ID: {user.id}</p>
));
}}>
Submit
</button>
<p>{ content }</p>
</div>
</div>
)
}
export default SearchUserForm;
Can anyone help with this?
One more question is that my data seems to return undefined everytime I execute the query. Is there something wrong with my query?
Here are the query:
const searchUserQ = gql`
query User($name: String!){
user(name: $name){
id
name
email
}
}
`;
Thanks and appreciate on the help!
According to the Rules of Hooks:
Don’t call Hooks from regular JavaScript functions. Instead, you can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks (we’ll learn about them on the next page).
If you need to manually call a query manually ins response to an event, use the Apollo client directly. You can use useApolloClient to get an instance of the client inside your component:
const SearchUserForm = () => {
const client = useApolloClient();
...
return (
...
<button onClick={async (event) => {
try {
const { data } = client.query({
query: searchUserQ,
variables: { name: event.target.value },
});
// Do something with data, like set state
catch (e) {
// Handle errors
}
}} />
You can also still use useQuery, like this:
const SearchUserForm = () => {
const [name, setName] = useState('')
const { data, loading, error } = useQuery(searchUserQ, {
variables: { name },
});
...
return (
...
<button onClick={async (event) => {
setName(event.target.value)
...
}} />
You can use the useLazyQuery method and expose your data object to your entire component.
import {useLazyQuery} from '#apollo/react-hooks';
// - etc. -
const SearchUserForm = () => {
// note: keep the query hook on the top level in the component
const [runQuery, { data, loading, error }] = useLazyQuery(searchUserQ);
// you can now just use the data as you would regularly do:
if (data) {
console.log(data);
}
return (
<div id="edit-user">
<div className="field">
<label htmlFor="name">Search UserName</label>
<input
type="text"
id="name"
onChange={(event) => {name = event.target.value }} />
<button onClick={
(event) => {
event.preventDefault();
// query is executed here
runQuery({
variables: { name }, // note: name = property shorthand
suspend: false
})
}}>
// - etc. -
);
}
As opposed to doing the useQuery, the useLazyQuery method will only be executed on the click.
At the point where you are able to pass the 'name' value as a parameter.
If for example you would use the useQuery instead, and have a parameter that is required in your query (i.e. String!), the useQuery method will provide you with an error. Because on component render it will try to run that query directly without the required parameter because at that period of time it's not available yet.
I found problem to my second answer, should just wrap an if-statement before executing it, here is the complete code:
import React, {useState} from 'react'
import {useQuery} from 'react-apollo-hooks';
import {searchUserQ} from '../queries/queries';
const SearchUserForm = () => {
const [name, setName] = useState('');
const { data, error, loading } = useQuery(searchUserQ, {
variables: { name }
});
let content;
let sName;
if (data.user !== undefined && data.user !== null) {
if (loading) { content = 'Loading User...' }
if (error) { content = `Error Occur: ${error}` }
const user = data.user;
content = `Username: ${ user.name } ID: ${ user.id }`
}
return (
<div id="edit-user">
<div className="field">
<label htmlFor="name">Search UserName</label>
<input type="text" id="name" onChange={(event) => {
sName = event.target.value;
}}/>
<button onClick={(event) => {
setName(sName);
}} value={name}>
Search
</button>
</div>
<div>
<p>{ content }</p>
</div>
</div>
)
};
export default SearchUserForm;