I'm trying to create a callable component to check if device is mobile or desktop
import { useMediaQuery } from 'react-responsive'
const CheckDeviceScreen = () => {
return useMediaQuery({
query: '(min-device-width: 1224px)'
})
}
export default CheckDeviceScreen;
Then I'm passing that props to a Route
import CheckDeviceScreen from "./Others/CheckDeviceScreen";
<Route exact path="/"> <Home isDesktop = {<CheckDeviceScreen/>}/></Route>
then in my Home page
const Home = ({isMobile}) => (
{!isMobile &&
//display something
}
)
But isMobile always return true even I'm viewing it at different device. What am I doing wrong?
What you created is not called a "callable component" but simply just a function. Because react components should return JSX.
Keeping that in mind, then in your code should be
import CheckDeviceScreen from "./Others/CheckDeviceScreen";
<Route exact path="/"> <Home isDesktop = {CheckDeviceScreen()}/></Route>
if you tried console.log(<CheckDeviceScreen />) vs console.log(CheckDeviceScreen()) you will find that the first returns an object which will always have a true condition when checked with !isMobile. But the other console.log will return the returned calling of the function, which is the check condition that you want: true or false
Related
I'm trying to create protected routes that are only viable while user is logged in, but I have trouble getting loggedIn state in ProtectedRoutes component, it's always set to false thus redirecting to "/login". What am I not getting correctly here?
App.tsx
interface loginContextInterface {
loggedIn: boolean;
setLoggedIn: (value: (((prevState: boolean) => boolean) | boolean)) => void;
}
export const LoginContext = createContext({} as loginContextInterface);
export default function App() {
const [ loggedIn, setLoggedIn ] = useState(false)
useEffect(() => {
console.log("before", loggedIn)
isLoggedIn().then((r) => {
console.log("R", r)
setLoggedIn(r)
})
console.log("after", loggedIn)
}, [loggedIn])
return (
<LoginContext.Provider value={{loggedIn, setLoggedIn}}>
<Router>
<MenuHeader />
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/tasks" element={<ProtectedRoutes/>}>
<Route path="/" element={<Tasks/>}/>
</Route>
<Route path="/login" element={<Login />}/>
<Route path="/logout" element={<Logout />}/>
<Route path="/register" element={<Register/>}/>
</Routes>
</Router>
</LoginContext.Provider>
);
}
ProtectedRoutes.tsx
export const ProtectedRoutes = () =>{
const location = useLocation();
const {loggedIn} = useContext(LoginContext)
console.log("protected", loggedIn)
return (
loggedIn ? <Outlet/> : <Navigate to={"/login"} replace state={{location}}/>
);
}
Edit:
isLoggedIn just authenticates that the user is logged in via cookie using api on the server side. Added logging
Produces these after trying to access /tasks route and redirecting me to /login again
VM109:236 protected false
App.tsx:21 before false
App.tsx:26 after false
App.tsx:21 before false
App.tsx:26 after false
2App.tsx:23 R true
App.tsx:21 before true
App.tsx:26 after true
App.tsx:23 R true
There is an issue with the useEffect hook, using the loggedIn state value as the dependency. You should not use dependencies that are unconditionally updated by the hook callback. My guess here is that you wanted to do an initial authentication check when the app mounts. You can remove loggedIn from the dependency since it's not referenced at all.
useEffect(() => {
isLoggedIn().then(setLoggedIn);
}, []);
I suggest also using an initial loggedIn state value that doesn't match either the authenticated or unauthenticated states, i.e. something other than true|false. This is so the ProtectedRoutes can conditionally render null or some loading indicator while any pending authentication checks are in-flight and there isn't any confirmed authentication state already saved in state.
Update the context to declare loggedIn optional.
interface loginContextInterface {
loggedIn?: boolean;
setLoggedIn: React.Dispatch<boolean>;
}
Update App to have an initially undefined loggedIn state value.
const [loggedIn, setLoggedIn] = useState<boolean | undefined>();
Update ProtectedRoutes to check for the undefined loggedIn state to render a loading indicator and not immediately bounce the user to the login route.
const ProtectedRoutes = () => {
const location = useLocation();
const { loggedIn } = useContext(LoginContext);
if (loggedIn === undefined) {
return <>Checking authentication...</>; // example
}
return loggedIn ? (
<Outlet />
) : (
<Navigate to={"/login"} replace state={{ location }} />
);
};
Also in App, remove/move the "/tasks" path from the layout route rendering the ProtectedRoutes component to the nested route rendering the Tasks component. The reason is that it's invalid to nest the absolute path "/" in "/tasks".
<Route element={<ProtectedRoutes />}>
<Route path="/tasks" element={<Tasks />} />
</Route>
It's not recommended to reset the dependency inside the useEffect(), it may cause an infinite loop.
useEffect(() => {
// loggedIn will be update here and trigger the useEffect agan
isLoggedIn().then((r) => setLoggedIn(r))
}, [loggedIn])
What does the console.log(loggedIn) and console.log(r) show? I'm guessing isLoggedIn returns false, loggedIn is set to false initially so useEffect not being triggered again and it remains as false
I have component AppRouter which is placed inside <BrowserRouter> and returns public or private <Route> components depending on whether the user is authenticated. Private routes are returning inside <PageWrapper> component. It contains header and sidebar, so routes are rendering in main part of this wrapper.
I am having those exceptions: React has detected a change in the order of Hooks called by AppRouter. & Rendered more hooks than previous render
Console
This is AppRouter:
export const AppRouter = () => {
const user = useAppSelector(state => state.authReducer.user)
const dispath = useAppDispatch();
useEffect(() => {
const userData = getUser()
if (userData !== null) {
dispath(authSlice.actions.updateUser(userData))
}
}, [])
const routes: IRoute[] = user === undefined ? publicRoutes : privateRoutes
let indexElement: IRoute | undefined
const routeComponents = (
<Routes>
{routes.map<React.ReactNode>((route: IRoute) => {
if (route.index === true) {
indexElement = route
}
return <Route
path={route.path}
element={route.component()}
key={route.path}
/>
})}
{
indexElement !== undefined && <Route path='*' element={<Navigate to={indexElement.path} replace />} />
}
</Routes>
)
if (user === undefined) {
return routeComponents
}
return (
<PageWrapper>
{routeComponents}
</PageWrapper>
)
}
This exception is thrown when user is authenticated and react renders component from private route(private route is only one now). It started to throw when I added useEffect(arrow function with console.log and empty dependecies array) to this private component. If i remove useEffect from this component - exceptions will not be thrown. I tried to change routes.map to privateRoutes.map - then exceptions does not throw, but I can't understand the reason why it works so.
Project is react + typescript + redux toolkit
I have the React/Typescript component below. It works as expected, but I'm trying to prove that with a test (using testing-library). I want to ensure that it correctly receives the level parameter and renders it to the page:
import React from "react";
import { useParams } from "react-router-dom";
type QuestionGridParams = {
level: string;
};
export const QuestionGrid = () => {
const { level } = useParams<QuestionGridParams>();
return (
<>
<header>
<h1>Level {level}</h1>
</header>
</>
);
};
I'd expected to do this by stubbing the context, or the function that returns the context, so that it always returns a specified value for the parameter. None of my searching has given a solution that I can get to work. Closest I have come is the test below, which only proves that the component works if no parameter is passed in. I want to pass in a level of 4, for example, and assert that there is an element with the text Level 4.
I don't think that this is the approach I need, it just happens to be the only way for now I can get the test not to error when useParams() is invoked..
test("page renders in the absence of a 'level' parameter", () => {
const layout = render(
<Router history={createMemoryHistory()}>
<QuestionGrid/>
</Router>
);
const header: any = layout.getByText("Level");
expect(header).toBeInTheDocument();
});
Any help getting past this mindblock would be greatly appreciated.
OK, so this worked:
test("page reads and renders the 'level' path parameter ", () => {
const layout = render(
<Router history={createMemoryHistory()}>
<Route path={"/:level"}>
<QuestionGrid/>
</Route>
</Router>
);
history.push("/4");
const header: any = layout.getByText("Level 4");
expect(header).toBeInTheDocument();
});
or more elegantly
test("page reads and renders the 'level' path parameter ", () => {
const layout = render(
<MemoryRouter initialEntries={["/4"]}>
<Route path={"/:level"}>
<QuestionGrid/>
</Route>
</MemoryRouter>
);
const header: any = layout.getByText("Level 4");
expect(header).toBeInTheDocument();
});
No stubbing/mocking, just dynamically creating a real route. Welcome to React, me!
Thanks #jonrsharpe and #r3wt
I've made a quick test that looks like this :
<Router>
<Switch>
<Route path="/foo" render={ () => <TestComponent key="test" data="1" /> } />
<Route path="/bar" render={ () => <TestComponent key="test" data="2" /> } />
</switch
</Router>
Where the TestComponent looks like this :
const TestComponent = withRouter(({ history, data }) => {
const [ value, setValue ] = useState(null);
const loading = !value;
useEffect(() => {
setTimeout(() => {
setValue(data);
history.push('/bar');
}, 1000 );
}, [ data ]);
console.log("**VAL", data, value);
return <div>
{ data } / { loading ? "LOADING" : value }
</div>;
});
So, when I hit the route /foo, the component loads, wait a second, then redirect to the other route using the same component (albeit a different instance).
The debug shows
**VAL 1 null
**VAL 1 1
**VAL 2 null // <-- expected : VAL 2 1
**VAL 2 2
**VAL 2 2
I would've expected React to see that both are the same component and not create a different instance. How can I have the same component respond to two different routes?
You can have one Route component have multiple paths, so multiple paths will result in the same component. More specifically the path prop of Route can take a string or string array. See docs here: https://reactrouter.com/web/api/Route/path-string-string
Then you can use one of the react-router hooks to determine which route you are on and render whatever differences you need based on that. Alternatively, you could use URL Parameters or Query Parameters.
I want to send information to second page if the user is logged in . I would like use Context to that.
Something about my code :
const Login = () => {
...
const [logged, setLogged] = React.useState(0);
...
const log = () => {
if (tempLogin.login === "Login" && tempLogin.password == "Haslo") {
setLogged(1);
}
...
return (
{logged == 1 && (
<Redirect to="/page" />
)}
I want to send logged to /page but i don't know how . None guide help me .Page is actually empty React file.
There are 2 ways handle that:
Passing state to route(as described in docs):
{logged == 1 && (
<Redirect to={{ path: "/page", state: { isLoggedIn: true } }} />
)}
And your component under /page route will access that flag as this.props.location.state.isLoggedIn
Utilize some global app state(Redux, Context API with <Provider> at root level or anything alike).
To me second option is better for keeping auth information:
Probably not only one target component will want to check if user is authorized
I'd expect you will need to store some authorization data to send with new requests(like JWT token) so just boolean flah accessible in single component would not be enough.
some operation on auth information like say logout() or refreshToken() will be probably needed in different components not in single one.
But finally it's up to you.
Thanks skyboyer
I solved this problem with Context method .I will try tell you how i do this becouse maybe someone will have the same problem
I created new file
import React from "react";
import { Redirect } from "react-router";
const LoginInfo = React.createContext();
export const LoginInfoProvider = props => {
const [infoLog, setInfoLog] = React.useState("");
const login = name => {
setInfoLog(name);
};
const logout = () => {
setInfoLog("old");
};
const { children } = props;
return (
<LoginInfo.Provider
value={{
login: login,
logout: logout,
infolog: infoLog
}}
>
{children}
</LoginInfo.Provider>
);
};
export const LoginInfoConsumer = LoginInfo.Consumer;
In App.js add LoginInfoProvider
<Router>
<LoginInfoProvider>
<Route exact path="/" component={Login} />
<Route path="/register" component={Register} />
<Route path="/page" component={Page} />
</LoginInfoProvider>
</Router>
In page with login (parts of code in my question) i added LoginInfoConsumer
<LoginInfoConsumer>