I have a functional component that sets state with an onSubmit function. The problem is that the setState function won't update the user state in time for it to be passed down to the user prop before the authenticated state is changed to true. Here is the basic gist of my component:
import React, { useState } from 'react';
import axios from 'axios';
import { LoginForm } from './LoginForm';
import { LoggedIn } from './LoggedIn';
const { login } = require('../utils/login');
const { getUser } = require('../utils/getUser');
export const Splash = () => {
const [user, setUser] = useState(null);
const [authenticated, setAuthenticated] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const _handleEmail = (e) => {
setEmail(e.target.value);
};
const _handlePass = (e) => {
setPassword(e.target.value);
};
const _handleSubmit = async (e) => {
e.preventDefault();
const token = await login(email, password);
const fetchUser = await getUser(token);
setAuthenticated(true);
setUser(fetchUser);
console.log(fetchUser) <---- logs user fine
console.log(user) <----- logs empty object
};
const _handleLogout = async (e) => {
const logout = await
axios.get('http://localhost:5000/api/v1/auth/logout');
console.log(
`Logout status: ${logout.status}, Success: ${logout.data.success}`
);
setAuthenticated(false);
setUser({});
};
return (
<div>
{!authenticated ? (
<LoginForm
handleEmail={_handleEmail}
handlePass={_handlePass}
handleSubmit={_handleSubmit}
/>
) : (
<LoggedIn
user={user}
authenticated={authenticated}
logout={_handleLogout}
/>
)}
</div>
);
};
For whatever reason, my setAuthenticated function will change the authenticated state from false to true, hence triggering the render of <LoggedIn />, but my user state will not update in time to be passed down to <LoggedIn /> through the user prop. I suspect the authenticated prop I send down isn't updated either. Thoughts?
Thanks!
EDIT: Here's my <LoggedIn /> component and it's <LoggedInNav />component.
/// LoggedIn.js
export const LoggedIn = ({ logout, user, authenticated }) => {
LoggedIn.propTypes = {
logout: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
authenticated: PropTypes.bool.isRequired
};
const [loggedInUser, setUser] = useState({});
const [loggedInAuth, setAuthenticated] = useState(false);
useEffect(() => {
try {
if (user) {
setAuthenticated(true);
setUser(user);
}
} catch (err) {
console.log(err);
}
}, []);
return (
<div>
{!authenticated ? (
<Spinner animation='border' variant='primary' />
) : (
<LoggedinNav navUser={user} logoutButton={logout} />
)}
</div>
);
};
// LoggedInNav.js
export const LoggedinNav = ({ navUser, logoutButton }) => {
LoggedinNav.propTypes = {
navUser: PropTypes.object.isRequired,
logoutButton: PropTypes.func.isRequired
};
return (
<div>
<Navbar bg='light' variant='light'>
<Navbar.Brand href='#home'>
<img
src={logo}
width='120'
height='auto'
className='d-inline-block align-top ml-3'
alt='React Bootstrap logo'
/>
</Navbar.Brand>
<Nav className='mr-auto'>
<Nav.Link href='#feedback'>Submit Feedback</Nav.Link>
</Nav>
<Navbar.Collapse className='justify-content-end mr-4'>
<Navbar.Text>
Signed in as: {navUser.name.first} {navUser.name.last} - Role:{' '}
{navUser.role}
</Navbar.Text>
</Navbar.Collapse>
<Button onClick={logoutButton} variant='outline-primary mr-4'>
Logout
</Button>
</Navbar>
</div>
);
};
According to React Docs, useState, same as setState, triggers a new render.
Next render, the new value will be in user.
Because you are in an async method, youre out of the loop, and cannot see new state value in the next line.
See codesandbox poc:
In it the async function awaits 2 seconds and then set state.
You can see in the console that the render is triggered after the set state, the render sees the new state, but the async method prints the old one.
useState, same as setState, are syncronous, meaning they will directly trigger a render.
For async work, you can use useEffect
Related
I have made a custom hook that takes url and fetches the data in json format. But when I am trying to assign the data into const users using use state, I getting the error :
'Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop'
Here is the component from where I am trying to assign:
import React, { useState } from "react";
import useFetch from "./fetchData";
import Users from "./Users";
const ASSIGN5 = () => {
const [users, setUsers] = useState();
const { data, isLoading, error } = useFetch(
"https://jsonplaceholder.typicode.com/users"
);
setUsers(data);
return (
<div className="container">
<h1 className="">Users Management App</h1>
{isLoading && <p>Loading users...</p>}
{error && <p>{error}</p>}
<Search onHandleSearch={handleSearch} />
{users && <Users users={users} />}
</div>
);
};
export default ASSIGN5;
And here is the useFetch hook:
import React, { useEffect, useState } from "react";
const useFetch = (url) => {
const [data, setData] = useState([]);
const [isloading, setIsloading] = useState(true);
const [error, setError] = useState();
useEffect(() => {
fetch(url)
.then((res) => {
if (!res.ok) {
throw Error("Fetching unsucessful");
} else {
return res.json();
}
})
.then((data) => {
setData(data);
setIsloading(false);
setError(null);
})
.catch((error) => {
setError(error.message);
setIsloading(false);
});
}, [url]);
return { data, isloading, error };
};
export default useFetch;
But it runs fine when I use data directly without assigning but I need to because have to filter the data using functions
I am expecting that the data will assigned to the const users
Don't call state setters unconditionally in the component body or that'll trigger infinite renders.
It appears you don't need the users state at all because it's just an alias of the data array returned by your useFetch hook.
const ASSIGN5 = () => {
const { data, isLoading, error } = useFetch(
"https://jsonplaceholder.typicode.com/users"
);
return (
<div className="container">
<h1 className="">Users Management App</h1>
{isLoading && <p>Loading users...</p>}
{error && <p>{error}</p>}
<Search onHandleSearch={handleSearch} />
{data?.length && <Users users={data} />}
</div>
);
};
If you want to rename it you can use
const { data: users, isLoading, error } = useFetch(...);
// now you can replace `data` with `users`
Search and handleSearch weren't defined but I assume those are in your actual code somewhere.
Components are typically PascalCase, so ASSIGN5 should be Assign5.
I want to create a todo, but after create a todo I want to not refresh all the list, but when I create a post request it also creates refresh and get request
const App = () => {
const dispatch = useDispatch()
const [hamburgerMenu, setHamburgerMenu] = useState(false);
const { authReducer } = useSelector(state => state)
useEffect(() => {
dispatch(refreshToken())
}, [dispatch])
return (
<Router>
<Alert />
<Navbar
hamburgerMenu={hamburgerMenu}
setHamburgerMenu={setHamburgerMenu}
/>
<Menu
hamburgerMenu={hamburgerMenu}
setHamburgerMenu={setHamburgerMenu}
/>
<Routes>
<Route exact path="/login" element={<Login />} />
<Route exact path="/register" element={<Register />} />
{<Route exact path="/" element={!authReducer?.access_token ? <HomepageLogout/> : <Homepage/>} /> }
</Routes>
</Router>
)
}
export default App
import { useEffect } from "react"
import TodoList from "../components/TodoList"
import { useSelector, useDispatch } from "react-redux"
import { getTodos } from "../redux/actions/todoAction"
import './styles/homepage.scss'
const Homepage = () => {
const dispatch = useDispatch()
const { authReducer, todoReducer } = useSelector((state) => state)
const userId = authReducer?.user?.userId
const access_token = authReducer?.access_token
useEffect(() => {
userId && dispatch(getTodos(userId))
}, [userId, dispatch ]);
const todoList = todoReducer?.data
return (
<div style={{ width: "100%", height: "100vh", display: 'flex', flexDirection: 'column', alignItems: "center", justifyContent: "center" }}>
<TodoList todoList={todoList} access_token={access_token} />
</div>
)
}
export default Homepage
import Todo from "./Todo"
import "./styles/todoList.scss"
import TodoForm from "./TodoForm"
const TodoList = ({ todoList, access_token, setIsAdded }) => {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div className="todoListContainer">
<h1 className="title2">What are you planing for today?</h1>
<ul className="todoListContainer__listContainer">
{todoList &&
todoList?.map((todo) => {
return (
<Todo todoList={todoList} todo={todo} key={todo?.todoId} access_token={access_token} />)
})
}
</ul>
<TodoForm todoList={todoList} />
</div>
</div>
)
}
export default TodoList
import { useState } from 'react'
import { useSelector, useDispatch } from "react-redux"
import { createTodo } from "../redux/actions/todoAction"
import "./styles/todoForm.scss"
const TodoForm = ({ todoList }) => {
const dispatch = useDispatch()
const { authReducer } = useSelector(state => state)
const token = authReducer?.access_token
const userId = authReducer?.user?.userId
const initialState = {
text: "",
todoUserId: userId
}
const handleSubmit = (e) => {
userId && dispatch(createTodo(todo, token, todoList))
}
const handleChange = (e) => {
const { value, name } = e.target
setTodo({
...todo,
[name]: value
})
}
const [todo, setTodo] = useState(initialState)
return (
<form className="todoForm" onSubmit={handleSubmit}>
<input
placeholder="Please add something.."
id="text"
name="text"
type="text"
value={todo?.text}
onChange={handleChange}
/>
<input
id="todoUserId"
name="todoUserId"
value={todo?.todoUserId}
readOnly
style={{ display: 'none' }}
/>
<button type="submit">
Add
</button>
</form>
)
}
export default TodoForm
TODO ACTION
export const createTodo = (todo, token, todoList) => async (dispatch) => {
try {
const res = await postAPI("/todo/create", todo, token);
dispatch({
type: TODO,
payload: {
status: "success",
message: "All todos found",
data: todoList.append(todo)
}
})
dispatch({ type: ALERT, payload: { success: res.data.message } })
} catch (error) {
dispatch({ type: ALERT, payload: { errors: error?.response?.data.message } })
}
};
POSTAPI
export const postAPI = async (url, post, token) => {
const res = await axios.post(`/api/${url}`, post, {
headers: { token: token }
})
return res
}
I have tried todoList with useState() also different array methods in payload in action but still it creates get and refresh request. It is bad for me because when I create a todo, access_token becomes for a second undefined, so the list disappears and comes another component. I want to add todo under the list without refreshing the page
Here my logs after create a todo
POST /api/todo/create 200 286.656 ms - 249
GET /api/refresh_token 200 200.426 ms - 408
GET /api/refresh_token 304 3.712 ms - -
GET /api/todo/get/emrekrt163395 200 4.840 ms - 7491
GET /api/todo/get/emrekrt163395 304 10.143 ms - -
It seems the page is reloading because the form is being submitted when clicking the "add" button. The default form action is not being prevented. handleSubmit should call preventDefault on the passed onSubmit event object to prevent the default form action from submitting the form and reloading the page (and entire React application).
Example:
const handleSubmit = (e) => {
e.preventDefault(); // <-- prevent submitting the form
if (userId) {
dispatch(createTodo(todo, token, todoList));
}
}
I have a custom hook(useData) that takes query as an argument and then returns data and runtime(time to fetch the data from the API). But I need access to the runtime to my Editor component when I click on the run button. Right now what is happening is when I click on run button(inside Editor.js), it sets the query to the App component using the setter function and then it passes that query to the Table component and then calls the custom hook using that query and then table make use of that data. but I want the runtime in the Editor component, not in the Table component. I know I can call useData hook in the Editor component but my editor component gets rerender every time when we write on the editor, so It calls the useData() hook on each change.
If I create a context using this hook then I can able to access the runtime and data wherever I want.
Anyone, please help me how to convert that to context!
App.js code
import React, { useState } from "react";
import "./assets/output.css";
import Footer from "./components/layouts/Footer";
import Navbar from "./components/layouts/Navbar";
import Sidebar from "./components/layouts/Sidebar";
import TableSection from "./components/table/TableSection";
import Editor from "./components/editor/Editor";
const App = () => {
const [query, setQuery] = useState("");
const [value, setValue] = useState("select * from customers");
return (
<>
<div className="grid grid-cols-layout-desktop grid-rows-layout-desktop bg-gray-600 h-screen">
<Navbar />
<Sidebar setQuery={setQuery} setValue={setValue} />
<Editor setQuery={setQuery} value={value} setValue={setValue} />
{query ? <TableSection query={query} /> : null}
<Footer />
</div>
</>
);
};
export default App;
Editor.js
import React from "react";
import AceEditor from "react-ace";
import "ace-builds/src-min-noconflict/ext-language_tools";
import "ace-builds/src-min-noconflict/mode-mysql";
import "ace-builds/src-noconflict/theme-github";
import useData from "../../hooks/useData";
const Editor = ({ setQuery, value, setValue }) => {
const { runtime } = useData();
const onChange = (newValue) => {
setValue(newValue);
};
const onSubmit = () => {
var Z = value.toLowerCase().slice(value.indexOf("from") + "from".length);
setQuery(Z.split(" ")[1]);
};
return (
<div className="col-start-2 col-end-3 row-start-2 row-end-3 m-6">
<AceEditor
aria-label="query editor input"
mode="mysql"
theme="github"
name={Math.floor(Math.random() * 100000).toString()}
fontSize={16}
minLines={15}
maxLines={10}
width="100%"
showPrintMargin={false}
showGutter
placeholder="Write your Query here..."
editorProps={{ $blockScrolling: true }}
setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
}}
value={value}
onChange={onChange}
showLineNumbers
/>
<div className="">
<button
className="bg-white text-gray-800 rounded-md font-semibold px-4 py-2 my-4"
onClick={onSubmit}
>
<i className="fas fa-play"></i> Run SQL
</button>
</div>
</div>
);
};
export default Editor;
Hook code:
import { useEffect, useState } from "react";
import alasql from "alasql";
import toast from "react-hot-toast";
import TABLE_NAMES from "../utils/tableNames";
const getURL = (name) =>
`https://raw.githubusercontent.com/graphql-compose/graphql-compose-examples/master/examples/northwind/data/csv/${name}.csv`;
const useData = (tableName) => {
const [data, setData] = useState([]);
const [error, setError] = useState(false);
const [runtime, setRuntime] = useState("");
const convertToJson = (data) => {
alasql
.promise("SELECT * FROM CSV(?, {headers: false, separator:','})", [data])
.then((data) => {
setData(data);
toast.success("Query run successfully");
})
.catch((e) => {
toast.error(e.message);
});
};
const fetchData = (tableName) => {
setData([]);
const name = TABLE_NAMES.find((name) => name === tableName);
if (name) {
setError(false);
fetch(getURL(tableName))
.then((res) => res.text())
.then((data) => convertToJson(data));
} else {
setError(true);
toast.error("Please enter a valid query");
}
};
useEffect(() => {
let t0 = performance.now(); //start time
fetchData(tableName);
let t1 = performance.now(); //end time
setRuntime(t1 - t0);
console.log(
"Time taken to execute add function:" + (t1 - t0) + " milliseconds"
);
}, [tableName]);
return { data, runtime, error };
};
export default useData;
If you want to create a context and use it wherever you want, you can create a context, and add the state in this component and pass it to the value prop in the Provider component.
See the sample code.
import React, { createContext, useState } from "react";
export const UserContext = createContext({});
export interface User {
uid: string;
email: string;
}
export const UserProvider = ({ children }: any) => {
const [user, setUser] = useState<User>();
// you can defined more hooks at here
return (
// Pass the data to the value prop for sharing data
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
Then wrap components with the provider function like this
<UserProvider>
<MyComponment1>
</MyComponment1>
<MyComponment2>
</MyComponment2>
<MyComponment3>
</MyComponment3>
</UserProvider>
At This time, Whatever Component in the UserProvider can access the context right now and you can use useContext hook to access the data that you pass in the value props
export const MyComponment1 = () => {
const { user, setUser } = useContext<any>(UserContext);
...
}
I am using useEffect to hit an api and display some data from the response.It works well in console but when i try to display the data in a component it throws an error.I am checking for the loading state though.I am showing the data after a i get a response then where does this null coming from
App.js file:
import { useState, useEffect } from 'react';
import Details from './components/Details/Details';
import Header from './components/Header/Header';
import GlobalStyle from './globalStyles';
const API_KEY = 'Private';
// const URL = `https://geo.ipify.org/api/v1?apiKey=${API_KEY}&ipAddress=${ip}`;
function App() {
const [ip, setIp] = useState('8.8.8.8');
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const res = await fetch(
`https://geo.ipify.org/api/v1?apiKey=${API_KEY}&ipAddress=${ip}`
);
const json = await res.json();
setResponse(json);
setIsLoading(false);
} catch (error) {
setError(error);
}
};
fetchData();
// return { response, error, isLoading };
}, [ip]);
return (
<>
<GlobalStyle />
<Header getIp={(q) => setIp(q)} />
<Details isLoading={isLoading} res={response} error={error} />
</>
);
}
export default App;
Header.js file:
import { useState } from 'react';
import { FaArrowRight } from 'react-icons/fa';
import React from 'react';
import { Form, FormInput, Head, HeadLine, Button } from './Header.elements';
// import { useFetch } from '../../useFetch';
const Header = ({ getIp }) => {
const [input, setInput] = useState('');
const onChange = (q) => {
setInput(q);
getIp(q);
};
return (
<>
{/* styled components */}
<Head>
<HeadLine>IP Address Tracker</HeadLine>
<Form
onSubmit={(e) => {
e.preventDefault();
onChange(input);
setInput('');
}}
>
<FormInput
value={input}
onChange={(e) => {
setInput(e.target.value);
}}
placeholder='Search for any IP address or Domain'
/>
<Button type='submit'>
<FaArrowRight />
</Button>
</Form>
</Head>
</>
);
};
export default Header;
Details.js file:
import React from 'react';
import { Box, Location } from './Details.elements';
const Details = ({ res, error, isLoading }) => {
console.log(res);
return isLoading ? (
<div>loading...</div>
) : (
<>
<Box>
<Location>{res.location.city}</Location>
</Box>
</>
);
};
export default Details;
the error it shows:
That happens because on the first render, Details component will receive isLoading=false and res=null, so it will try to render the box so it's throwing the error.
You can initialize isLoading as true.
const [isLoading, setIsLoading] = useState(true);
Or render the Location if res has some value.
<Box>
{res && <Location>{res.location.city}</Location>}
</Box>
According to React documentation :
https://reactjs.org/docs/hooks-reference.html
By default, effects run after every completed render, but you can
choose to fire them only when certain values have changed.
So your component is rendering at least once with isLoading as false before even the API call starts.
You have two choices here:
Set isLoading initial value to true
Add optional chaining res?.location.city
https://codesandbox.io/s/stackoverflow-67755606-uuhqk
I am trying to implement a Login and Logout functionality.
Everything is working fine, but when I click Logout, I get a blank screen until I refresh the page instead of Login component.
So far, I have tried this:
function App() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [user, setUser] = useState()
const handleLogout = () => {
setUser({});
setUsername("");
setPassword("");
localStorage.clear();
};
const handleSubmit = async e => {
e.preventDefault();
const user = { username, password };
const response = await axios.post(
"http://<ip-address>/api-token-auth/",
user
);
setUser(response.data.token)
localStorage.setItem('user', response.data.token)
};
useEffect(() => {
const loggedInUser = localStorage.getItem("user");
if (loggedInUser) {
const foundUser = (loggedInUser);
setUser(foundUser);
}
}, []);
if (user) {
return (
<Router>
<Navigation
logout={handleLogout}
user = {user}
/>
<Switch>
<Route exact path="/" component={PermissionApply}/>
</Switch>
</Router>
)
}
return (
<Login
setUsername={setUsername}
setPassword={setPassword}
handleSubmit={handleSubmit}
/>
);
}
And my Navigation component:
<a className="nav-link float-right" onClick={props.logout}>Log Out</a>
Any help on why this is happening and how to fix it would be constructive.
Your useEffect is running once when the component is loaded because of the [].
If you watch for state changes on user, then it should work properly.
useEffect(() => {
const loggedInUser = localStorage.getItem("user");
if (loggedInUser) {
const foundUser = (loggedInUser);
setUser(foundUser);
}
}, [user]);
Also, in the handleLogout() you are setting the user state to an empty dictionary instead of null.
const handleLogout = () => {
setUser(); // This ensures that the user state is null
setUsername("");
setPassword("");
localStorage.clear();
};