UseContext with React Router - reactjs

Shortly, when I try to use useState with useContext in one of my components, all pages just disappear. UseState in some reason block my Routers and I have no idea why... Can you tell me where is my mistake?
Some code below:
Index.js
export default function App() {
const [value, setValue] = useState(false) -----> here I set the state
return (
<BrowserRouter>
<UserContext.Provider value={{ value, setValue }}>
<Routes>
<Route path='/' element={<Layout />}>
<Route index element={<Home />} />
<Route path='Home' element={<Home />} />
<Route path='Menu' element={<Menu />} />
<Route path='Story' element={<Story />} />
<Route path='Coffee' element={<Coffee />} />
<Route path='Cart' element={<Cart />} />
</Route>
</Routes>
</UserContext.Provider>
</BrowserRouter>
)
}
// ReactDOM.render(<App />, document.getElementById("root"))
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)
Buy.js component
import { useState } from "react"
import { useContext } from "react"
import { UserContext } from "../../UserContext"
const Buy = () => {
const [buttonText, setButtonText] = useState("Add to cart")
const [isActive, setIsActive] = useState(false)
// const [value, setValue] = useContext(UserContext) --> after I declare state everything disappears
const addToCart = () => {
setIsActive((current) => !current)
// setValue(true)
if (isActive) {
setButtonText("Add to cart")
}
}
return (
<div>
<button
class='buy'
style={{
fontSize: isActive ? "0.8rem" : "1rem",
color: isActive ? "lightgray" : "white",
}}
onClick={() => {
addToCart()
}}
>
{buttonText}
</button>
</div>
)
}
export default Buy
UserContext.js
import { createContext } from "react"
export const UserContext = createContext(null)
Actually, I need this Context only for routes "Coffee" and "Cart", but if I wrap only this 2 routes will be the same problem and all is disappear. Should I maybe use Context in my Layout.jsx instead of Index.js? Thank you.
const Layout = () => {
return (
<>
<Navbar />
<Outlet />
<Footer/>
</>
);
};
export default Layout;
The errors in console:

Your context provides an object, not an array, so you should destructure using curly braces when you use it:
const { value, setValue } = useContext(UserContext);
Or if you want to keep this way of destructuring, you can provide an array instead:
<UserContext.Provider value={[value, setValue]}>

There are a few issues I see with the React code that I myself struggled with while learning React.
Issue #1
Your button in Buy.js has a class='buy' property. Rename that to className='buy' because that's just React's syntax.
Issue #2
Your button's onClick={} property should reference only the function's name, and should not call the function itself. Change the onClick to onClick={addToCart}. Do not add the anonymous arrow function, simply input the name of the function.
Possible Issue #3
Most of the conditional functionality you are looking for can be implemented with React's useEffect() hook. Change addToCart() in the following way:
const addToCart = () => {
setIsActive();
}
useEffect(() => {
if(isActive) {
setButtonText("Add to cart");
}
}, [isActive]);
Make sure to import useEffect() before using this.

First i cannot see your Buy.js component between Context.Provider tree.
Then please try to use as object to destruct state values, not array.

Related

Why forwarded refs with react-router-dom return null?

In my App.js I have a ref:
const canvasView1 = React.createRef();
...
<div ref={canvasView1}/>
And a route to a Homepage component with the ref as a prop:
<Route
index
path="/welcome"
element={<Homepage canvasView1={canvasView1}/>}
/>
Then, in Homepage.js I use forwardRef() and I log the forwarded ref :
export const Homepage = React.forwardRef((props, canvasView1) => {
useEffect(() => {
console.log('# canvasView1 Homepage.js :', canvasView1)
}, [canvasView1]);
...
}
But it returns null whereas in App.js it returns the object:
I've read the docs about refs forwarding and tried multiple syntax but it still doesn't work.
Since you are using React function components you'll want to use the useRef hook so the created ref is a stable reference. React.createRef() will create a brand new React ref reference each render cycle.
const canvasView1 = React.useRef();
The Homepage component is forwarding the special ref prop, not any of the other regular named props that may or may not hold a React ref value. Pass the canvasView1 ref on the Homepage component's ref prop so it's forwarded.
<Homepage ref={canvasView1} />
or update the Homepage component to access the passed canvasView1 prop.
export const Homepage = ({ canvasView1 }) => {
React.useEffect(() => {
console.log("# canvasView1 Homepage.js :", canvasView1);
}, [canvasView1]);
return <h1 ref={canvasView1}>Homepage</h1>;
});
Code:
export default function App() {
const canvasView1 = React.useRef();
React.useEffect(() => {
console.log("# canvasView1 App.js :", canvasView1);
}, [canvasView1]);
return (
<div className="App">
<div ref={canvasView1} />
<Routes>
<Route path="/" element={<Homepage ref={canvasView1} />} />
</Routes>
</div>
);
}

Router Router Switch causes unmount on updating route search/query strings

In my App.js I have a number of components wrapped in a Switch component from react-router-dom
App.js
import React from "react";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
const Test = Loadable({
loader: () => import("./Test"),
loading: () => <h1>LOADING....</h1>
});
const Test1 = Loadable({
loader: () => import("./Test1"),
loading: () => <h1>LOADING....</h1>
});
const Test2 = Loadable({
loader: () => import("./Test2"),
loading: () => <h1>LOADING....</h1>
});
const App = () => {
return (
<Switch>
<ProtectedRoute bgColour="blue" exact path="/" component={Test} />
<ProtectedRoute bgColour="red" exact path="/1" component={Test1} />
<ProtectedRoute bgColour="green" exact path="/2" component={Test2} />
</Switch>
);
};
export default App;
The ProtectedRoute component renders a Route component from react-router-dom passing in the specified component. It also has a HOC, which in my actual application checks the user is authenticated
ProtectedRoute.js
import React from "react";
import { Route } from "react-router-dom";
const withAuth = (Component) => {
return (props) => {
return <Component {...props} />;
};
};
const ProtectedRoute = ({ component, bgColour, ...args }) => {
return (
<div style={{ backgroundColor: bgColour || "transparent" }}>
<Route component={withAuth(component)} {...args} />
</div>
);
};
export default ProtectedRoute;
For each component, I have alerts setup to trigger on mount and unmount of the component. On a click on an element it updates the query string to a random number via history.push, however, this currently triggers an unmount, due to the Switch added in App.js, without the Switch there is no unmount. This is causing an issue in my application as an unmount is not desired behaviour and is causing issues with loading the correct data.
Test.js
import React, { useEffect } from "react";
import { useHistory } from "react-router-dom";
export default function Test() {
const history = useHistory();
useEffect(() => {
alert("MOUNTED BASE");
return () => {
alert("UNMOUNTED BASE");
};
}, []);
return (
<div>
<h1>TEST COMPONENT BASE - BLUE</h1>
<div
onClick={() =>
history.push({
pathname: history.location.pathname,
search: `?query=${Math.random().toFixed(2)}`
})
}
>
UPDATE QUERY STRING
</div>
<div onClick={() => history.push("/1")}>GO TO Next ROUTE</div>
</div>
);
}
I still want the functionality of the Switch but prevent the unmount on history.push, is this possible?
I have a CodeSandbox below to recreate this issue
Issue
Based on only the code you've provided, an issue I see is how every component the ProtectedRoute renders is decorated with the withAuth Higher Order Component. This results in a new component being created every time ProtectedRoute renders.
Solution
You want to only decorate the routed components with HOCs once prior to where they are used.
Example:
const ProtectedRoute = ({ bgColour, ...props }) => {
return (
<div style={{ backgroundColor: bgColour || "transparent" }}>
<Route {...props} />
</div>
);
};
...
import React from "react";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
import withAuth from "..path/to/withAuth";
// Decorate components with HOCs once out here
const Test = withAuth(Loadable({
loader: () => import("./Test"),
loading: () => <h1>LOADING....</h1>
}));
const Test1 = withAuth(Loadable({
loader: () => import("./Test1"),
loading: () => <h1>LOADING....</h1>
}));
const Test2 = withAuth(Loadable({
loader: () => import("./Test2"),
loading: () => <h1>LOADING....</h1>
}));
// Render decorated components in App
const App = () => {
return (
<Switch>
<ProtectedRoute bgColour="red" path="/1" component={Test1} />
<ProtectedRoute bgColour="green" path="/2" component={Test2} />
<ProtectedRoute bgColour="blue" path="/" component={Test} />
</Switch>
);
};

Changing props state using api causes infinite loop

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>
);

React cannot pass props to parent component

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>
);
}

How to use the useHook in the component rather than passing the value returned from it as a prop to the components using react and typescript?

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>
);
}

Resources