Context is reset to null when browser URL is manually changed - javascript

I have created AuthContext that holds currently logged in user:
// auth-context.ts
export interface IAuthContext {
auth: IMe | null;
setAuth: (user: IMe | null) => void;
}
const AuthContext = React.createContext<IAuthContext>({
auth: null,
setAuth: (auth: IMe | null) => {}
});
export default AuthContext;
The simplified main rendered App component where I use the context provider looks like this:
const App: FC = () => {
const [auth, setAuth] = useState<IMe | null>(null); // problematic line
return (
<AuthContext.Provider value={{auth, setAuth}}>
<BrowserRouter>
<Routes>
<Route path='/' element={<Main/>}/>
<Route path="/login" element={<Login/>}/>
<Route path="/register" element={<Register/>}/>
<Route path="/about" element={<About/>}/>
</Routes>
</BrowserRouter>
</AuthContext.Provider>
);
}
Overall, the application works fine.
However, when I navigate the application by manual URL changes in the web browser, followed-up by hitting the Enter (therefore: page refresh), then there is a problem with auth context reset to null...
I think when I change the URL manually then the whole App component is re-rendered and therefore the AuthContext state is reseted to null by the problematic line.
The idea is that the Login component is using the setAuth inside of it after the form-related promise is positively resolved.
Any tip how can I protect myself from that re-render problem, and therefore context resettng to default null value? I think this scenario is kind of basic, but I am beginner in frontend stuff. Thank you!

Related

How to navigate programmatically react-router-dom v6

I came back to react world after a few years. And things certainly have changed for good. I'm using MemoryRouter for my app. And I can navigate fine by using Link. But useNaviate hook is not working as expected. It does nothing on the page. Could you please help me here? Here is my code:
Router:
<MemoryRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</MemoryRouter>
Here is how I'm trying the navigation:
function Home() {
// demo purpose
const navigate = useNavigate()
navigate('/dashboard')
}
I'm not sure if I'm using it right, or if I need to do something else here.
The code is calling navigate as an unintentional side-effect directly in the function component body.
Either call navigate from a component lifecycle or callback to issue an imperative navigation action:
function Home() {
const navigate = useNavigate()
useEffect(() => {
if (/* some condition */) {
navigate('/dashboard');
}
}, [/* dependencies? /*]);
...
}
Or conditionally render the Navigate component to a declarative navigation action:
function Home() {
...
if (/* some condition */) {
return <Navigate to="/dashboard" />;
};
...
}
The problem was that I was calling navigate directly when the component was rendering. It should either be called in an event, or it should be called in useEffect hook.
Make your navigate in function call or in useEffect like this:
function Home() {
// demo purpose
const navigate = useNavigate()
useEffect(() => {
navigate('/dashboard')
}, []);
}

While using react-router 5 with redux 7 react-router <Link> is not resetting state after going to new route

I'm using the following versions:
`"react-router": "^5.2.0",`
`"react-router-domreact-router": "^5.2.0",`
Not sure if my current setup is React-router 5 friendly or not, I was using a version prior to v5 before this.
The problem in this example is with <Route component={withTracker(InterviewContainer)} path="/interviews/companies/:companyId" /> and <Link/>
Here's my scenario:
Home page loads with a list of company links
Click on a company <Link /> which routes me to /interviews/companies/:companyId
Page loads fine, I see images, etc. for that particular company
Click browser's Back button
Click on a different company <Link /> that points to a different companyId
Problem: for #5, when the company page initially loads, it's loading with stale images and data for some reason. So in other words, I'm seeing the previous company's data & images from step #2 briefly until my React hook makes a new call to get data for this new CompanyId and repaints the browser with the right data (data for the companyId represented in the new route)
index.tsx (note the use of BrowserRouter here)
import { BrowserRouter as Router } from 'react-router-dom';
//...more code and then:
render(
<>
<div className="Site">
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>
</div>
<Footer />
</>,
);
App.ts
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
...more code and then here are my routes:
<Switch>
<Route component={withTracker(HomePageContainer)} exact path="/" />
<Route
path="/companies/:companyId/details"
render={(props: RouteComponentProps<{ companyId: string }>) => (
<CompanyDetailContainer {...props} fetchCompanyNew={fetchCompanyNew} httpRequest={Request} useFetchCompany={useFetchCompany} />
)}
/>
<Route component={withTracker(InterviewContainer)} path="/interviews/companies/:companyId" />
<Route component={withTracker(About)} path="/about" />
<Route component={withTracker(Container)} path="/" />
<Route component={withTracker(NotFound)} path="*" />
</Switch>
Here is how the company Link is coded:
Note: I am using Redux State
"react-redux": "^7.2.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
InterviewContainer.tsx (the parent that does the company fetching)
class InterviewContainer extends Component<PropsFromRedux & RouteComponentProps<{ companyId: string }>> {
componentDidMount() {
const { fetchCompany } = this.props;
const { companyId } = this.props.match.params;
fetchCompany(companyId);
}
render() {
const { company } = this.props;
return (company && <Interview className="ft-interview" company={company} />) || null;
}
}
const mapState = (state: RootState) => ({
company: state.company.company,
});
const mapDispatch = {
fetchCompany: fetchCompanyFromJSON,
};
const connector = connect(mapState, mapDispatch);
type PropsFromRedux = ConnectedProps<typeof connector>;
export default withRouter(connect(mapState, mapDispatch)(InterviewContainer));
LinkItem.tsx (one of the children rendered by InterviewContainer and receives the company from InterviewContainer)
render() {
const { company } = this.props,
uri = company.notInterviewed ? `companies/${company.id}/details` : `/interviews/companies/${company.id}`,
className = `margin-top-10 margin-bottom-10 ${company.notInterviewed ? 'ft-company-not-interviewed' : ''}`;
const link = (
<Link className={className} id={company.id.toString()} to={uri}>
<span id="company-name">{company.name}</span>
</Link>
);
}
I think I may have to reset Redux state on route change. I see people in the past have used LOCATION_CHANGE but that's outdated and that's a constant provided by third party redux libs that are no longer supported. So not sure how to do that with Redux v7+
So I think I just need a way to detect a location change and then somehow update my react store to reset company (set company: state.company.company, to undefined from my redux action)
I know things like this can be cumbersome. Have you tried passing in state with the Link as <Link to={uri} state={...someState} />. Then wherever it is loading it should rerender or reset props according to that. Maybe throw some skeleton loaders or conditional rendering logic.

How to access to Home component even though not logged in?

In a React project, I have created certain components which have access only when logged in or else would be redirected to Login Page. While I was told to make few changes that are, the user should have access to the Home page even though not logged in. For accessing other components, the user must have a token. See the following code for reference
export const UserContext = createContext();
const Routing = () => {
const history = useHistory()
const { state, dispatch } = useContext(UserContext)
const [value, setValue] = useState(null)
const user = sessionStorage.getItem('token')
useEffect(() => {
if(user) {
dispatch({ type: "USER", payload: user })
} else {
history.push('/login')
}}, [])
return (
<>
<Router>
<Switch>
{/* Give access to user even though not logged in or has token */}
<Route exact path="/" component="Home" />
{/* I won't let user access this Component, unless token is available */}
<Route exact path="/videoCall" component="VideoCall" />
</Switch>
</Router>
</>
)
}
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<UserContext.Provider value={{state, dispatch}}>
<Router>
<Switch>
<Route exact path="/login" component={LoginPage} />
<Routing />
</Switch>
</Router>
</UserContext.Provider>
)
}
export default App;
So, what could be the best possible solution, give access to the Home page even though not logged in, but, redirect the user to Login Page when trying to access other components like VideoCall.
Maybe something like this will help - a Router comonent of your own, which will check whether the user is authenticated and will decide whether to let him in the component or not. If you want the Home component to be rendered, just make it render, and components you don't want to be rendered if user is not logged in, just add a check in the component (or in the router component of you own) whether the user is authenticated...
BTW, every render, the user const is assigned the sessionstorage.getItem() value... every render. If you want it to happen only once, or only when a specfic variable changes - you should totally use useMemo (const user = useMemo(()=>sessionStorage.getItem(), [])) (https://reactjs.org/docs/hooks-reference.html#usememo)

React: Unable to navigate views with router after changing the state of Context (useContext)

I have a simple setup to test the use of the useContext hook, when you want to change the context value in child components.
A simple Context is defined in its own file like such:
import React from 'react'
const DataContext = React.createContext({})
export const DataProvider = DataContext.Provider
export default DataContext
Then I wrap my router in a provider in a component that exposes its state to use as a reference for the ContextProvider, as such:
import { DataProvider } from './dataContext.js'
export default function App(props) {
const [data, setData] = useState("Hello!")
const value = { data, setData }
const hist = createBrowserHistory();
return (
<DataProvider value={value}>
<Router history={hist}>
<Switch>
<Route path="/admin" component={Admin} />
<Redirect from="/" to="/admin/services" />
</Switch>
</Router>
</DataProvider>
)
}
Finally I have two Views that I am able to navigate between initially, one of them showcasing the context value, as well as containing a button to change it:
export default function EndpointView(props) {
const { data, setData } = useContext(DataContext)
return (
<div>
<h1>{data}!</h1>
<Button onClick={() => setData(Math.random())}>Update context state</Button>
</div>
)
}
The functionality seems to work, as the showcases text is updated.
The problem is, when I have clicked the button, I can no longer navigate in my navbar, even though the url is changing. Any ideas as to why?
This is showcased in this picture, where the url is corresponding to the top-most item in the side bar, even though we are stuck in the "endpoint view"-component.
Edit:
So the routing works by including a switch in the Admin layout:
const switchRoutes = (
<Switch>
{routes.map((prop, key) => {
if (prop.layout === "/admin") {
return (
<Route
path={prop.layout + prop.path}
component={prop.component}
key={key}
/>
);
}
return null;
})}
<Redirect from="/admin" to="/admin/services" />
</Switch>
);
Where the routes (which we .map) are fetched from another file that looks like this:
const dashboardRoutes = [
{
path: "/services",
name: "Services view",
icon: AccountBalance,
component: ServicesView,
layout: "/admin"
},
{
path: "/endpoint",
name: "Endpoint view",
icon: FlashOn,
component: EndpointView,
layout: "/admin"
}
];
export default dashboardRoutes;
I was able to solve this issue.
I suspect the problem was that updating the state reloaded the root router component which caused some issues.
Instead I moved the DataProvider tag one step down the tree, to wrap the switch in the Admin component.

React Router v4: Sending requests when navigation changes

I'm coding an authentication with react-router v4 and I'm using the PrivateRoute with render props, like the documentation: Redirects (Auth)
What I'm trying to do is: Whenever the user navigates to a route, I want to dispatch an action to make a request to the backend to verify if he's logged in.
Like this:
// App.js
class App extends Component {
checkAuth = () => {
const { dispatch, } = this.props;
// callback to dispatch
}
render() {
const props = this.props;
return (
<Router>
<div className="App">
<Switch>
<Route exact path="/" component={Login} />
<PrivateRoute
exact
path="/dashboard"
component={Dashboard}
checkIsLoggedIn={this.checkAuth}
/>
{/* ... other private routes here */}
</Switch>
</div>
</Router>
);
}
In PrivateRoute.js I'm listening the route to check if it changes, but when a route changes, this function is called too many times, and that's a problem to dispatch an action to make a request.
// PrivateRoute.js
const PrivateRoute = ({ component: Component, auth, checkIsLoggedIn, ...rest }) => (
<Route
{...rest}
render={props => {
props.history.listen((location, action) => {
if (checkIsLoggedIn) {
// Here I check if the route changed, but it render too many times to make a request
checkIsLoggedIn(); // here is the callback props
}
});
if (auth.login.isLoggedIn) {
return <Component {...props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />
}
}
}
/>
);
I need a help to figure it out a good way to call the backend whenever the route changes.
Creating a Higher Order Component (HOC) is a very clean way to do this. This way, you won't need to create a separate PrivateRoute component, and it would take only one line of change to convert any Component from public to protected, or vice versa.
Something like this should work:
import React from 'react';
import { Redirect } from "react-router-dom";
export function withAuth(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
isUserLoggedIn: false,
isLoading: true
};
}
componentDidMount() {
// Check for authentication when the component is mounted
this.checkAuthentication();
}
checkAuthentication() {
// Put some logic here to check authentication
// You can make a server call if you wish,
// but it will be faster if you read the logged-in state
// from cookies or something.
// Making a server call before every protected component,
// will be very expensive, and will be a poor user experience.
this.setState({
isUserLoggedIn: true, // Set to true or false depending upon the result of your auth check logic
isLoading: false
});
}
render() {
// Optionally, you can add logic here to show a common loading animation,
// or anything really, while the component checks for auth status.
// You can also return null, if you don't want any special handling here.
if (this.state.isLoading) return (<LoadingAnimation />);
// This part will load your component if user is logged in,
// else it will redirect to the login route
if (this.state.isUserLoggedIn) {
return <WrappedComponent authData={this.state} {...this.props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />;
}
}
}
}
Once you have that component in place, all you need to do is use the HOC in any component that you wish to have protected. For example, in your case, the export line in your Dashboard file would be something like this:
/* Dashboard.js */
class Dashboard extends React.Component { ... }
export default withAuth(Dashboard);
and in your App, you can use a simple Route component:
<Route exact path='/dashboard' component={Dashboard} />
Your App does not need to care about which routes are protected, and which ones aren't. In fact, only the actual components need to know that they are protected.
Hope this helps. Cheers! :)

Categories