How to use Conditional Rendering and Redux in App js file? - javascript

I am trying to implement authentication flow with Redux in my app, I am using conditional rendering in my App.js file to show either app screen or authentication screen.
App.js:
<Provider store={store} >
<NavigationContainer>
{store.getState().auth.initializing && <Initializing />}
{store.getState().auth.isLoggedIn && (
<AppNavigator/>
)}
{!store.getState().auth.initializing &&
!store.getState().auth.isLoggedIn && (
<AuthenticationNavigator/>
)}
</NavigationContainer>
</Provider>
The problem is it doesn't react on changes in the state, when I press Login button on the login screen even though redux refreshes the state, the conditional rendering in App.js doesn't react anyhow and I still see login screen. If I use useState with isUserLoggedIn flag in the App.js, and pass callback to the components it works, but not with Redux.
Please help, what do I do wrong?

The Redux store.getState is only a function that returns the current state, it doesn't subscribe to changes to the store object. This only works when the component rendering your conditionals is rerendered.
What you can do is either use the subscribe method to subscribe to changes in the store, caching the state locally:
const [auth, setAuth] = React.useState(() => store.getState().auth);
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setAuth(store.getState().auth);
});
return unsubscribe;
}, []);
...
<NavigationContainer>
{auth.initializing && <Initializing />}
{auth.isLoggedIn && (
<AppNavigator/>
)}
{!.auth.initializing &&
!.auth.isLoggedIn && (
<AuthenticationNavigator/>
)}
</NavigationContainer>
Or you and use the useSelector from react-redux to effectively do the same thing:
const auth = useSelector(state => state.auth);
...
<NavigationContainer>
{auth.initializing && <Initializing />}
{auth.isLoggedIn && (
<AppNavigator/>
)}
{!.auth.initializing &&
!.auth.isLoggedIn && (
<AuthenticationNavigator/>
)}
</NavigationContainer>
Note that both of these solutions absolutely require the Redux store Provider to be moved higher in the ReactTree so that this component can consume the redux context and access the store's value.

Related

react-dom-router Link component doesnt work as expected

I have a react app with two pages--- the home and trip page. The trip page is rendered with the tripID passed in through the url. My app.js looks like :
function App() {
return (<>
<ThemeProvider theme={theme}>
<Router>
<Routes>
<Route exact path='/' element={<Home />} />
<Route path='/trip/:tripId' element={<TripComponent />} />
</Routes>
</Router>
</ThemeProvider>
</>
);
}
I have a global navbar with a menu of different tripIds as many Link to navigate to the TripComponent. When i am at the "/" path, and I navigate to "/trip/tripA", i have no problems. But when i am at "/trip/tripA" , and i want to navigate to "/trip/tripB", it doesnt work. I can see the url changing accordingly, but my TripComponent doesnt rerender with tripB's data. the code for my menu in my navbar looks like:
ReactDOM.createPortal(<>
<CustomModal setOpen={setShowTripList} title={"Saved Trips"}>
<List>
{trips && trips.length > 0 &&
trips.map((trip, index) => {
return (
<Link to={`/trip/${trip._id}`} >
<ListItemButton>
<ListItemText id={trip._id} primary={trip.title} />
</ListItemButton>
</Link>
)
})
}
</List>
</CustomModal>
</>
, document.getElementById("portal"))
I am confused as to why this is happening. When i press the Link to navigate to another URL, shouldn't it unmount and remount?
When the tripId route path parameter updates the routed element TripComponent isn't remounted, it is only rerendered. If there is some logic that depends on the tripId path parameter then TripComponent needs to "listen" for changes to that value. This is the same thing that would need to happen if a prop value changed.
Use a useEffect hook with a dependency on the tripId path parameter to issue side-effects based on the current value.
Example:
import { useParams } from 'react-router-dom';
...
const TripComponent = () => {
const { tripId } = useParams();
useEffect(() => {
// initial render or tripId updated
// do something with current tripId value
}, [tripId]);
...
};
I think the #Drew's answer is perfect.
But I'd like to put something additionally.
I suggest you to use useMemo hook.
...
const trip = useMemo(() => {
// something you want
}, [tripId])
...

How to prevent rerenders with react-router-dom and react-redux when changing route?

I have these routers
function Rutas(){
<Provider store={store}>
<Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
</Switch>
</Router>
</Provider>
}
This is my Sidebar
function Home(){
return (
<>
<NavLink to="/home">Home</NavLink>
<NavLink to="/about">About</NavLink>
</>
)
}
And this is the Home component
function Home(){
const dispatch = useDispatch();
const homeData = useSelector((data) => data.homeData);
React.useEffect(() => dispatch(getHomeDataAction),[dispatch])
return (
<>
{
homeData.map((res) => <span>{res.title}</span>)
}
</>
)
}
And this is the About component
function About(){
const dispatch = useDispatch();
const aboutData = useSelector((data) => data.aboutData);
React.useEffect(() => dispatch(getAboutDataAction),[dispatch])
return (
<>
{
aboutData.map((res) => <span>{res.title}</span>)
}
</>
)
}
On page load for the first time the Home component rendered, that's okay, when i change route to About component it's rendered too and this it's okay, but the problem it's when i change route again to the Home component it's rendered again and it call useEffect and dispatch again, I WANT TO PREVENT TO DISPATCH AGAIN WHEN THE ROUTE CHANGE BECOUSE I HAVE A LOT OF DATA AND IT TAKE A WHILE TO RENDERED AGAIN THE DATA FROM USESELECTOR AND THE UI IT'S SO SLOW.
Please tell me some solution or recommendations.
Thanks ☺
You could somehow memorize does getHomeDataAction action was dispatched or not.
const loadedData = useSelector((data) => data.homeData.loaded);
React.useEffect(() => {
if (!loadedData) {
dispatch(getHomeDataAction)
}
},[dispatch, loadedData])
All in all calling the getHomeDataAction action conditionally could help.
Apart from these changes you should extend your reducer.
After dispatching getHomeDataAction action the first time the value of the loaded property should be turned to true.

Protected routung in Reactjs

Aim : I have user logged in as ADMIN, user is in localstorage. Now i want to check
if User === "admin" return the Component else Redirect to main page.
Problem : Whenever i try to access the component (logged in as ADMIN) it Redirects me to main application page. What it should do is to let me in.
Here is the code, i think the problem is in If statement.
function App() {
const cart = useSelector((state) => state.cartReducer.cart);
const user = JSON.parse(localStorage.getItem("profile"));
return (
<Router>
<Switch>
<Route path="/updateProduct/:name/:id">
<Navbar cart={cart} user={user} />
if(user?.result?.role !== "admin") {<Redirect to="/" />} //here it is misbehaving
<ProductUpdate />
<Footer />
</Route>
</Switch>
</Router>
);
}
export default App;
Note. I have multiple components here, So how do i do it.
As far as i concern, you need to use ternary operator or short circuit inside route.
You can use this by consoling the output.
if(user?.result?.role !== "admin") {
console.log('here');
<Redirect to="/" />};
}
check if 'here' is showing in the console.

React: tracking history changes by history.push in common component

I have a Layout component which wraps the rest of my application. The default route is a page with multiple buttons, implemented as a small component called NavButton, which use history.push to go to a new route. Then in my Layout component, there are 2 buttons, but one of them should change depending on which route we are currently navigated to. So when we are on the main page, the button should say i.e. "Change Code", but on any other page, it should say "Back". I tried it in the following way:
Layout.tsx:
interface ILayoutProps {
children: ReactNode;
}
const Layout: React.FC<ILayoutProps> = ({ children }) => {
const currentRoute = useLocation();
useEffect(() => {
console.log(currentRoute);
}, [currentRoute]);
const logout = () => {
console.log('logout');
};
return (
<div>
<div>{children}</div>
<Grid container spacing={2}>
<Grid item xs={6}>
{currentRoute.pathname === '/' ? (
<NavButton displayName="Change Code" route="/change-code" variant="outlined" />
) : (
<Button variant="outlined" color="primary" fullWidth>
Back
</Button>
)}
</Grid>
...
</Grid>
</div>
);
};
export default Layout;
NavButton.tsx:
interface NavButtonProps {
displayName: string;
route: string;
variant?: 'text' | 'outlined' | 'contained';
}
const NavButton: React.FC<NavButtonProps> = ({ displayName, route, variant = 'contained' }) => {
const history = useHistory();
const navigatePage = () => {
history.push(route);
// Also tried it like this:
// setTimeout(() => history.push(route), 0);
};
return (
<Button
variant={variant}
color="primary"
onClick={() => navigatePage()}
fullWidth
>
{displayName}
</Button>
);
};
export default NavButton;
SO in my Layout, I am trying to keep track of location changes with the useLocation() hook, but when a button is pressed, and thus history.push(route) is called, it doesn't get detected by the hook in Layout. I've also tried to use the useHistory() hook and call the listen() method on it, but same problem. Also, my router is a BrowserRouter, not sure if that is of any help... What am I missing here?
So the issue was that I had my App component defined like this:
<Layout>
<Routes />
</Layout>
And my Routes looked like this:
<BrowserRouter>
<Switch>
<Route ...>
....
</Switch>
</BrowserRouter>
The useLocation() hook in my Layout wasn't working since it was not a child of my BrowserRouter. Changing it to be like that made the code work.
Only thing I find weird is that in my app, I didn't get an error for invalid hook call, but in the sandbox I did. Maybe I need to upgrade my React packages...

Pass props nicely to screens in react navigation

I want to pass a prop to the screen. When I try that inline e.g (props) => <Comp {...props} {...customProps} /> I get a warning message, that I shouldn't parse functions to that component property. Okay. I thought I'll just create functions for every component which needs custom props. It is working, but is there a better solution? Here is my component:
export default function Loading() {
const [loggedIn, setLoggedIn] = React.useState(false);
const Stack = createStackNavigator();
const authService: AuthService = new AuthService();
const authProps: IAuthProps = {
authService
};
/**
* Bind neccessary props to the login component
* #param props Props
*/
function LoginWithProps(props) {
return <Login {...props} {...authProps} />;
}
/**
* Bin neccessary props to the registration component
* #param props Props
*/
function RegistrationWithProps(props) {
return <Registration {...props} {...authProps} />;
}
return (
<>
{/*Show the app, when logged in*/}
{loggedIn === true ? (
<View>
<Text>Test</Text>
</View>
) : (
<Stack.Navigator
initialRouteName="Login"
screenOptions={{ headerShown: false, animationEnabled: false }}
>
<Stack.Screen name="Login" component={LoginWithProps} />
<Stack.Screen name="Registration" component={RegistrationWithProps} />
</Stack.Navigator>
)}
</>
);
}
```
Yours is not a good solution to the problem because new types of LoginWithProps and RegistrationWithProps components will be created every render, meaning old ones will be unmounting and new ones mounting every time. The same thing that happens when you pass a function, but without a warning
You can't pass props to these screens, as it is not the Loading component that is a direct parent to these screens. If you want to pass data to customize those components, you need to do it through navigation params, in 2 ways:
when navigating to a screen navigation.navigate('Login', { param: 'hello' })
by proviging initial params
.
<Stack.Screen
name="Login"
component={Loaing}
initialParams={{ param: 'hello' }}
/>
And read it in Login with props.route.params
Note though this is called initialParams for a reason - they are not reactive, changing them after component is mounted has no effect (it is also possible to change them from inside component). If you want to pass reactive params, use React Context or Redux
Passing parameters to routes

Categories

Resources