I create a let variable in my parent component with the boolean false. I pass that variable as a prop to the child component. I change the variable to true through a function in the parent component.
My problem is that the value of the prop in the child component is still false when I console.log it afterwards.
Parent:
function App() {
let success = false
const changePW = async ({password, repeatPW}) => {
success = true
console.log(`Success App0: ${success}`)
console.log('ChangePW')
return (
<BrowserRouter>
<div className="container">
<Header/>
<Route path='/' exact render={(props) => (
<>
<AddPasswort onChange = {changePW} success = {success}/>
<Footer />
</>
)}/>
<Route path='/about' component={About} />
<Route path='/success' component={Success} />
<Route path='/error' component={Error} />
</div>
</BrowserRouter>
);
}
export default App;
Child
const AddPasswort = ({onChange,success}) => {
const[password, setPassword] = useState('')
const[repeatPW, setRepeatPW] = useState('')
// create a history object
const history = useHistory()
const onSubmit = async (e) => {
e.preventDefault();
await onChange({password, repeatPW})
console.log(success)
// navigate to the success page
if (success){
console.log('success')
history.push("/success")
}
else{
console.log('error')
history.push("/error")
}
}
}
...
}
export default withRouter(AddPasswort);
I thought the problem was that the function does not wait for onChange to finish so I made it asynch, but that did not resolve the issue
because success is not a state,
only changing state will re-render component.
try
const [success, setSuccess] = useState(false);
to change the value of success to true , do
setState(true)
this should solve your problem
Related
I don't know why, changing the props state inside useEffect causes infinite loop of errors. I used them first locally declaring within the function without using props which was running ok.
EDIT:
Home.js
import Axios from "axios";
import React, { useEffect, useState } from "react";
function Home(props) {
// const [details, setDetails] = useState({});
// const [login, setLogin] = useState(false);
useEffect(() => {
try {
const data = localStorage.getItem("expensesAccDetails");
if (data) {
Axios.post("http://localhost:3001/eachCollectionData", {
collection: data,
}).then((res) => {
if (res.data.err) {
console.log("Error");
} else {
console.log(res.data[0]);
props.setLogin(true);
props.setUserdetails(res.data[0]);
}
});
}
} catch (err) {
console.log(err);
}
}, []);
return props.login ? (
<div>
<div>Welcome {props.setUserdetails.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
}
export default Home;
App.js
function App() {
const [login, setLogin] = useState(false);
const [userdetails, setUserdetails] = useState({});
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
setLogin={setLogin}
login={login}
setUserdetails={setUserdetails}
userdetails={userdetails}
/>
<Bars login={login} />
</>
}
/>
<Routes>
<Router>
);
Here I initialized the states directly in App.js so I don't have to declare it on every page for the route renders. I just passed them as props to every component.
I suggest to create a componente Home with the post and two sub-component inside:
const Home = () => {
const [userDetails, setUserDetails] = useState({});
const [login, setLogin] = useState(false);
useEffect(() => {
// api call
}, []);
return (
<>
<Welcome login={login} details={userDetails} />
<Bars login={login} details={userDetails} />
</>
);
};
where Welcome is the following:
const Welcome = ({ userdetails, login }) => (
<>
login ? (
<div>
<div>Welcome {userdetails.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
</>
);
A better solution is to use only one state variable:
const [userDetails, setUserDetails] = useState(null);
and test if userDetails is null as you test login is true.
An alternative if you have to maintain the call as you write before, you can use two state as the follow:
function App() {
const [userdetails, setUserdetails] = useState(null);
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
setUserdetails={setUserdetails}
/>
<Bars login={!!userdetails} />
</>
}
/>
<Routes>
<Router>
);
and on Home component use a local state:
const Home = ({setUserdetails}) => {
const [userDetailsLocal, setUserDetailsLocal] = useState(null);
useEffect(() => {
// api call
// ... on response received:
setUserdetails(res.data[0]);
setUserDetailsLocal(res.data[0]);
// ...
}, []);
userDetailsLocal ? (
<div>
<div>Welcome {userDetailsLocal.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
};
I advise to follow Max arquitecture for your solution. the problem lies in the Router behavior. React Router is not part of React core, so you must use it outside your react logic.
from documentation of React Router:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render.
https://v5.reactrouter.com/web/api/Route/component
Edit:
ok, you make me write it. A solution could be like:
function App() {
const [login, setLogin] = useState(false);
const [userdetails, setUserdetails] = useState({});
useEffect(() => {
try {
const data = localStorage.getItem("expensesAccDetails");
if (data) {
Axios.post("http://localhost:3001/eachCollectionData", {
collection: data,
}).then((res) => {
if (res.data.err) {
console.log("Error");
} else {
console.log(res.data[0]);
setLogin(true);
setUserdetails(res.data[0]);
}
});
}
} catch (err) {
console.log(err);
}
}, []);
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
login={login}
userdetails={userdetails}
/>
<Bars login={login} />
</>
}
/>
<Routes>
<Router>
);
GOAL: Give data to App comp from Register comp, then from App comp to Chat comp
Register --> App --> Chat
Additional info: Register is taking a username and then passing it to Chat comp to render as username
Or should I just pass the value to url params and then get it?
The answers I looked up were suggesting creating redux or were from class components
import Chat from "./components/chat";
import Register from "./components/register";
import { BrowserRouter, Route, Routes } from "react-router-dom";
function App(props) {
console.log(props);
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Register />}>
<Route
path="chat"
element={(props) => <Chat {...props} data={true} />}
/>
</Route>
<Route
path="*"
element={
<main style={{ padding: "1rem" }}>
<p>404 :)</p>
</main>
}
/>
</Routes>
</BrowserRouter>
);
}
Register component:
import { useState } from "react";
import { useNavigate } from "react-router-dom";
export default function Register() {
const navigate = useNavigate();
const [name, setName] = useState("");
const generatedName = `user#${Math.floor(Math.random() * 1000000 + 1)}`;
const handleSubmit = () => {
if (name === "") {
setName(generatedName);
}
navigate("/chat", { replace: true });
HERE I WANT TO RETURN THE DATA TO PARENT//return "hello parent";
};
return (
<div>
Enter username:{" "}
<input
placeholder={generatedName}
onChange={(e) => setName(e.target.value)}
></input>
<button onClick={() => handleSubmit()}>enter</button>
</div>
);
}
You can set up a data state in your app component using a useState hook and pass a reference to a setter function which modifies your data and set it to a value and pass both of them to the Register component. Also, pass the data to your chat component as you would need it. You can try like below,
In App component,
...
function App(props) {
const [data,setData] = useState('');
const myDataSetterFunction = (dataToBeSet) => {
setData(dataToBeSet);
}
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Register intialData={data} myDataSetterFunction={myDataSetterFunction} />}>
<Route
path="chat"
element={(props) => <Chat dataToBeSupplied={data} {...props} data={true} />}
/>
</Route>
<Route
....
The Register component can modify the data and use the passed setter Function to modify the state of the hook. As soon as the data is modified there will be a re-render triggered which will pass the data or you can pass the data only if the data changes from the initial value to be more safer.
Inside the Register.js use the props to call the function passed to set the data inside the app doing something like this.
props.myDataSetterFunction( ...dataToBeReturned... );
Also, Remember to make sure the data has some value before passing it to the chat and using it.
In here, you can use queryParams when you want to switch to a new component:
navigate({
pathname: '/chat',
search: '?message=hello parent',
});
In React we cannot directly pass the props from the children to the parents. you can pass a function as a prop from the parent component to a child component, then call that function in the child component.
If Register comp took the input name then render Chat comp. Not sure if I needed this react-router-dom thing
function Register() {
const navigate = useNavigate();
const [passedUsername, setForPassedUsername] = useState(false);
const [name, setName] = useState("");
const generatedName = `user#${Math.floor(Math.random() * 1000000 + 1)}`;
const handleSubmit = () => {
if (name === "") {
setName(generatedName);
}
setForPassedUsername(true);
navigate("/chat", { replace: true });
return "hello parent";
};
return (
<div>
{passedUsername ? (
<Chat data={name}></Chat>
) : (
<div>
Enter username:{" "}
<input
placeholder={generatedName}
onChange={(e) => setName(e.target.value)}
></input>
<button onClick={() => handleSubmit()}>enter</button>
</div>
)}
</div>
);
}
So I have a straight forward app that requires you to login to see a dashboard. I've based my auth flow off of https://reactrouter.com/web/example/auth-workflow which in return bases their flow off of https://usehooks.com/useAuth/
Currently, when a user logs in it calls a function within the context provider to sign in and that function updates the state of the context with the user data retrieved from the server. This is reflected in React dev tools under my context providers as shown in the teacher attribute:
When the context state has successfully been updated I then use useHistory().push("dashboard/main") from the react-router API to go to the dashboard page. The dashboard is a consumer of the context provider but the teacher value is still null when I try rendering the page- even though React dev tools clearly shows the value has been updated. When I log in again, the dashboard will successfully render, so, ultimately, it takes two context updates in order for my Dashboard to reflect the changes and render. See my following code snippets (irrelevant code has been redacted):
App.js
const App = () => {
return (
<AuthProvider>
<div className="App">
<Switch>
<Route path="/" exact >
<Home setIsFetching={setIsFetching} />
</Route>
<ProtectedRoute path="/dashboard/:page" >
<Dashboard
handleToaster={handleToaster}
/>
</ProtectedRoute>
<ProtectedRoute path="/dashboard">
<Redirect to="/dashboard/main"/>
</ProtectedRoute>
<Route path="*">
<PageNotFound/>
</Route>
</Switch>
<Toaster display={toaster.display} setDisplay={(displayed) => setToaster({...toaster, display: displayed})}>{toaster.body}</Toaster>
</div>
</AuthProvider>
);}
AuthProvider.js
const AuthProvider = ({children}) => {
const auth = useProvideAuth();
return(
<TeacherContext.Provider value={auth}>
{children}
</TeacherContext.Provider>
);};
AuthHooks.js
export const TeacherContext = createContext();
export const useProvideAuth = () => {
const [teacher, setTeacher] = useState(null);
const memoizedTeacher = useMemo(() => ({teacher}), [teacher]);
const signin = (data) => {
fetch(`/api/authenticate`, {method: "POST", body: JSON.stringify(data), headers: JSON_HEADER})
.then(response => Promise.all([response.ok, response.json()]))
.then(([ok, body]) => {
if(ok){
setTeacher(body);
}else{
return {...body};
}
})
.catch(() => alert(SERVER_ERROR));
};
const register = (data) => {
fetch(`/api/createuser`, {method: "POST", body: JSON.stringify(data), headers: JSON_HEADER})
.then(response => Promise.all([response.ok, response.json()]))
.then(([ok, body]) => {
if(ok){
setTeacher(body);
}else{
return {...body};
}
})
.catch(() => alert(SERVER_ERROR));
};
const refreshTeacher = async () => {
let resp = await fetch("/api/teacher");
if (!resp.ok)
throw new Error(SERVER_ERROR);
else
await resp.json().then(data => {
setTeacher(data);
});
};
const signout = () => {
STORAGE.clear();
setTeacher(null);
};
return {
...memoizedTeacher,
setTeacher,
signin,
signout,
refreshTeacher,
register
};
};
export const useAuth = () => {
return useContext(TeacherContext);
};
ProtectedRoute.js
const ProtectedRoute = ({children, path}) => {
let auth = useAuth();
return (
<Route path={path}>
{
auth.teacher
? children
: <Redirect to="/"/>
}
</Route>
);
};
Home.js
const Home = ({setIsFetching}) => {
let teacherObject = useAuth();
let history = useHistory();
const handleFormSubmission = (e) => {
e.preventDefault();
const isLoginForm = modalContent === "login";
const data = isLoginForm ? loginObject : registrationObject;
const potentialSignInErrors = isLoginForm ?
teacherObject.signin(data) : teacherObject.register(data);
if(potentialSignInErrors)
setErrors(potentialSignInErrors);
else{
*******MY ATTEMPT TO PUSH TO THE DASHBOARD AFTER USING TEACHEROBJECT.SIGNIN********
history.replace("/dashboard/main");
}
};
};)};
Dashboard.js
const Dashboard = ({handleToaster}) => {
const [expanded, setExpanded] = useState(true);
return (
<div className={"dashboardwrapper"}>
<Sidebar
expanded={expanded}
setExpanded={setExpanded}
/>
<div className={"dash-main-wrapper"}>
<DashNav/>
<Switch>
<Route path="/dashboard/classroom" exact>
<Classroom handleToaster={handleToaster} />
</Route>
<Route path="/dashboard/progressreport" exact>
<ProgressReport/>
</Route>
<Route path="/dashboard/help" exact>
<Help/>
</Route>
<Route path="/dashboard/goalcenter" exact>
<GoalCenter />
</Route>
<Route path="/dashboard/goalcenter/create" exact>
<CreateGoal />
</Route>
<Route path="/dashboard/profile" exact>
<Profile />
</Route>
<Route path="/dashboard/test" exact>
<Test />
</Route>
<Route path="/dashboard/main" exact>
<DashMain/>
</Route>
</Switch>
</div>
</div>
);
};
Let me know if there's anything that stands out to you that would be preventing my Dashboard from rendering with the updated context values the first time instead of having to update it twice. Do let me know if you need more insight into my code or if I missed something- I'm also fairly new to SO. Also, any pointers on the structure of my app would be greatly appreciated as this is my first React project. Thank you.
I think the problem is in the handleFormSubmission function:
const handleFormSubmission = (e) => {
e.preventDefault();
const isLoginForm = modalContent === "login";
const data = isLoginForm ? loginObject : registrationObject;
const potentialSignInErrors = isLoginForm ?
teacherObject.signin(data) : teacherObject.register(data);
if(potentialSignInErrors)
setErrors(potentialSignInErrors);
else{
history.replace("/dashboard/main");
}
};
You call teacherObject.signin(data) or teacherObject.register(data) and then you sequentially change the history state.
The problem is that you can't be sure the teacher state has been updated, before history.replace is called.
I've made a simplified version of your home component to give an example how you could approach the problem
function handleSignin(auth) {
auth.signin("data...");
}
const Home = () => {
const auth = useAuth();
useEffect(() => {
if (auth.teacher !== null) {
// state has updated and teacher is defined, do stuff
}
}, [auth]);
return <button onClick={() => handleSignin(auth)}>Sign In</button>;
};
So when auth changes, check if teacher has a value and do something with it.
i want to use useHook within the component itself rather than passing it as a prop to each component using react and typescript.
what i am trying to do?
I have a useHook named useRefresh which returns isLoading state. This isLoading state is used to display a loading indicator in each of the pages.
so i have three pages and whenever this isLoading is true should display a loading indicator in these pages.
below is my code,
function App(){
const user = useGetUser();
return (
<Router>
<Switch>
<Route
path="/"
render={props: any => (
user ? (<Main {...props} />) : (
<LoginPage/>
);
)}
</Route>
</Switch>
</Router>
);
}
export function useLoad() {
const { refetch: refetchItems } = useGetItems();
const { refetch: refetchOwnedItems } = useListOwnedItems();
return async function() {
await refreshCompany();
refetchItems();
refetchOwnedItems();
};
}
function useAnother(Id: string) {
const [compId, setCompId] = React.useState(undefined);
const [isLoading, setIsLoading] = React.useState(false);
const comp = useCurrentComp(Id);
const load = useLoad();
if (comp && comp.id !== compId) {
setCompId(comp.id);
const prevCompId = compId !== undefined;
if (prevCompId) {
setIsLoading(true);
load().then(() => {
setIsLoading(false);
});
}
}
}
function Main ({user}: Props) {
useAnother(user.id);
return (
<Router>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</Router>
);
}
function FirstComp () {
return(
<Wrapper>
//some jsx
</Wrapper>
);
}
function SecondComp () {
return(
<Wrapper>
//some jsx
</Wrapper>
);
}
Now i want to pass isLoading state to each of the components in Main component....so i have passed it like below,
function Main ({user}: Props) {
const isLoading = useAnother(user.id); //fetching isLoading here from useHook
return (
<Router>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp isLoading={isLoading} {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp isLoading={isLoading} {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</Router>
);
}
function FirstComp ({isLoading}: Props) {
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}
function SecondComp ({isLoading}: Props) {
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}
This works. but doesnt seem like a right approach to me.. i dont want to pass this isLoading state as a prop to each of these components. there are more than 10 of them.
is there someway that i can do it other way than this. could someone help me with this. thanks.
The most common solution is to create a context that wraps the entire tree of components. This context holds the state that your hook pulls in
////LoadingContext.tsx
const LoadingContext = createContext();
const LoadingContextProvider = () => {
const [isLoading, setIsLoading] = useState(false);
return (
<LoadingContextProvider.Provider
value={{
isLoading,
setIsLoading
}}
/>
)
}
export const useLoading = () => useContext(LoadingContext);
You need to wrap the context around anything that will be calling useLoading:
import { LoadingContextProvider } from './LoadingContext' //or wherever this is relative to Main.tsx
<LoadingContextProvider>
<Router>
...(router stuff)
</Router>
</LoadingContextProvider>
Now you can call useLoading in your lower-level components.
//in another file defining a lower-level component:
import { useLoading } from '../../LoadingContext' //or wherever the context stuff is relative to this component definition
const FirstComp = () =>
const [isLoading, setIsLoading] = useLoading();
const handleClick = () => {
setIsLoading(true);
callMyApi().then(() => setIsLoading(false));
}
if(isLoading){
return <LoadingGif />
}
else{
return <div onClick={handleClick}>Click me!</div>
}
)}
What you would like to accomplish here is called global state. There are many ways to do it, but I think the simplest is the native React Context API.
All you have to do is create a ContextProvider and then use the useContext hook inside your components to access the values it provides.
Here is an example that should work for your case:
Main.js
export const LoadingContext = React.createContext(true); //creating and exporting the context
function Main ({user}: Props) {
const isLoading = useAnother(user.id); //fetching isLoading here from useHook
return (
<LoadingContext.Provider value={isLoading}> {/* providing the value to the children */}
<Router>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</Router>
</LoadingContext.Provider>
);
}
export default Main;
Other components
import {LoadingContext} from './Main.js'
function FirstComp ({}: Props) {
const isLoading = useContext(LoadingContext); //accessing the value
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}
function SecondComp ({}: Props) {
const isLoading = useContext(LoadingContext); //accessing the value
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}
I have a component which once once it rendered I need to redirect a user to another path and I'm using useEffect hook of react but it's getting rendered over and over and over without stopping:
const App: FunctionComponent<{}> = () => {
const [message, setMessage] = useState("");
useEffect(() => {
if (condition) {
setMessage("you are being redirected");
setTimeout(() => {
location.href = `http://localhost:4000/myurl`;
}, 2000);
} else {
setMessage("you are logged in");
setTimeout(() => {
<Redirect to={"/pages"} />;
}, 2000);
}
}, [message]);
return (
<>
{message}
<BrowserRouter>
<Route exact path="/login/stb" render={() => <Code />} />
</BrowserRouter>
</>
);
};
export default App;
It looks like setMessage is setting a message state variable in the component. This occurs during every run of useEffect. State changes will cause your component to rerender.
Basically, the flow causing the loop is this:
Initial component render
useEffect is run
message state is updated
Component rerenders due to state change
useEffect is run as it's trigged on message change
Back to step 3
If you want useEffect to only run on initial render, and redirect after a user has logged in, you could change it to something like this:
const [loggedIn, setLoggedIn] = useState(false);
useEffect(() => {
if (someConditionHere) {
setLoggedIn(true);
}, []);
return (
loggedIn ? <Redirect to={"/pages"} /> :
<BrowserRouter>
<Route exact path="/login/stb" render={() => <Code />} />
</BrowserRouter>
);
I don't know everything about the setup, so that's simplifying and making some assumptions.