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

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...

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])
...

MUI theme light/dark switch in header(AppBar)

I'm trying to find a way to switch between dark and light theme in my web app.
I want to add the switch in my AppBar component which is inside my header but I'm not sure how to get it to work on all of the web app and not just the header.
AppBar.js :
//imports ...
const AppBar = () =>{
const [theme, setTheme] = useState(false);
const changeTheme = () => {
setTheme(!theme);
};
//rest of the code....
<Box sx={{ flexGrow: 0 }}>
<Switch checked={theme} onChange={changeTheme} />
</Box>
};
export default AppBar;
here is the code from the Header.js
const Header = () => {
return (
<header>
<AppBar />
</header>
);
};
export default Header;
so the header is just rendering one component one could get rid of it if it was necessary.
and here is my App.js (routes)
//imports ...
//themes ...
const app = ()=>{
return (
<React.Fragment>
<ThemeProvider theme={theme ? lightTheme : darkTheme}>
<CssBaseline />
<Router>
<Header />
<Routes>
//Routes ...
</Routes>
<Footer />
</Router>
</ThemeProvider>
</React.Fragment>
);
}
export default App;
I'd really appreciate the help and Thanks in advance!
you can store the value of theme inside context or redux store and change it using a dispatch function once that value changes the whole component related to it will re render !, so your component will change the value inside context ! rather than having the value stuck inside one component.

How can I get or see prop value in my other component?

Experts am in the learning stage and I wanted to know how can I can get the text input value from the search bar and pass it to my another component where I have an API URL as a search query.
I have seen many videos which use props and I guess that’s the correct way if not wrong to send the data between components. However, I tried to use that but I have an issue. I can not see the props value which I have passed from my nav component (textfiled value) to the product component.
any help how can I do that.
Please if you can provide a source code explanation would be great.
Also please find the relevant code below:
Please see the full code here https://codesandbox.io/s/determined-nightingale-m2mtn?file=/src/App.js
This is the code on my nav component and I want the onchange value to be passed to the product component.
const Navbar = () =>{
const[searchText, setSearchText] = useState();
const handleChange = (event) =>{
setSearchText(event.target.value)
}
return (
<>
<div className="nav_main">
<div className="logo_main">
<Link exact to='/home' className="logo" > Movie </Link>
</div>
<div className="search_main">
<TextField
id="input-with-icon-textfield"
className= "searchTextStyle"
size= "small"
placeholder="search"
variant="standard"
value = {searchText}
onChange = {handleChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{color:'white'}} />
</InputAdornment>
),
}}
/>
</div>
<div className="nav_menu">
<NavLink exact activeClassName= "active_link" to='/home' className="navitems" > Home </NavLink>
<NavLink exact activeClassName= "active_link" to='/about'className="navitems" > About </NavLink>
<NavLink exact activeClassName= "active_link" to = '/products' className="navitems" > Products </NavLink>
<IconButton className="account_icon">
<AccountCircleIcon fontSize="small"/>
</IconButton>
</div>
</div>
</>
)
};
export default Navbar;
The Product component:
const Produts = (props) =>{
console.log(props.value);
return (
<>
<h1>{`http://api.themoviedb.org/3/search/movie?api_key=Mykey&query=${props.value}`}</h1>
</>
)
};
export default Produts;
Thanks
how are you?
What happens is, it's okay, you are getting the data from de input, but you are not passing it forward to the other component where you want to use that data.
How can you do that?
As they are 'sibling' components, i recommend you to put the handleChange and the useState where you are going to save the data in the FATHER component, pass them through props for the component where the user types, and then pass the stored data also via props for the component where you are going to use it.
Let me give you a pratical example of how you can do it:
On the App you handle the state and provide it for the components via props.
function App() {
const [searchText, setSearchText] = useState();
const handleChange = (event) => {
setSearchText(event.target.value);
};
return (
<>
<Navbar handleChange={handleChange} searchText={searchText}/>
<Switch>
<Route path="/products"
render= { (props) => <Produts {...props} searchText={searchText} /> }
/>
</Switch>
</>
);
}
in the NavBar component you get them via props, destructuring it:
const Navbar = ({handleChange, searchText}) => {
return (
<>
<div className="nav_main">
<div className="logo_main">
<Link exact to="/home" className="logo">
{" "}
Movie{" "}
</Link>
</div>
and in the Produts component you get them also via props:
const Produts = (props) => {
console.log(props.searchText);
const classes = useStyles();
const [movieData, setMovieData] = useState([
// the below is an fake api data which needs to be deleted when final disgn is ready and add real api from useEffect
{
adult: false,
observe that, before you were getting "props.value" but the name that you get in the component is the same name that you used to pass it from the provider component, in this case 'searchText'.
tl;dr
You need to pass them via props, choosing a name, and you get them from the other side using the same name. To get them you can either use 'props.PROP_NAME' or you can use the destructuring method and get the name directly.
Also, to pass a props through a Router rendered component you have to use this syntax:
render= { (props) => <Produts {...props} PROP_NAME={VARIABLE_YOU_ARE_USING}
On the NavBar and the Produts components i used different methods so you can see that you can use both for getting the props information.
I hope that can help, anyway i'm available for further explanations.
The NavLink has additional properties which can be used. The 'to' attribute needn't be only a string value. For example:
<NavLink exact activeClassName= "active_link" to = '/products' className="navitems" > Products </NavLink>
can be rewritten as
to={{
pathName: '/products',
someProps:{data: 'Place any Data here'}
}}
className="navitems" > Products </NavLink>
Then in your products component you should be able to retrieve the data using this.props.location.someProps.data.
Take a look at these articles:
https://reactrouter.com/web/api/Link/to-object,
https://medium.com/#bopaiahmd.mca/how-to-pass-props-using-link-and-navlink-in-react-router-v4-75dc1d9507b4
They outline exactly how this should be used.

How to show the dialog everywhere in the app on clicking a button using react and typescript?

i want to display a dialog on clicking additem and addbooks button using react and typescript.
what i am trying to do?
I want to display a dialog on clicking additem button or addbooks button . this dialog will have hide button. on clicking this hide button the dialog should never appear again for the session.
Below is the code,
function MainComponent () {
const [showDialog, setShowDialog] = React.useState(false);
const openDialog = () => {
setShowDialog(true);
};
const hideDialog = () => {
setShowDialog(false);
};
return (
<Route
path="/items"
render={routeProps => (
<Layout>
<Home
showDialog={showDialog}
openDialog={openDialog}
hideDialog={hideDialog}
{...routeProps}
/>
{showDialog && (
<Dialog
hideDialog={hideDialog}
/>
)}
</Layout>
)}
/>
<Route
path="/items/:Id/books/:bookId"
render={routeProps => (
<Layout>
<Books
openDialog={openDialog}
{...routeProps}
/>
{showDialog && (
<Dialog
hideDialog={hideDialog}
/>
)}
</Layout>
)}
</>
)
function Home ({openDialog}: Props) {
return (
<button Onclick={openDialog}>AddItem</Button>
)
}
function Books ({openDialog}: Props){
return (
<button onClick={openDialog}>AddBooks</Button>
)
}
function MessageDialog({hideDialog}: Props) {
return (
<button onClick={hideDialog}>hide</button>
)
}
Now the question is as you see i am rendering MessageDialog in two places based on showDialog value. if users clicks additems button the dialog is displayed and when user to navigates to other view and clicks addbooks button the dialog is displayed.
somehow i feel this is not the right approach or something is missing...
How can i create a global dialog component that is accessible anywhere from my app or using toastify or some better approach. could someone help me with this. thanks.
I think your code is pretty good, but I can think of two minor things that could potentially improve it.
Firstly, you have some duplicate code in your main component. You should be able to move the Dialog outside of the routes. Like so:
return (<>
{showDialog && (
<Dialog
hideDialog={hideDialog}
/>
)}
<Route ... />
<Route ... />
</>)
This will render your dialog no matter which route is matched.
Secondly, instead of passing openDialog and hideDialog callbacks as props, you could create a React context. This is optional, and dependning on the case this might not be desired. A React context is slightly more complex than just a callback, so it adds complexity to your code. The benefit is that the callbacks doesn't need to be passed down through the props of every child component, which makes the component structure cleaner if you have a large component tree.
To create a React context for this use case, you could do the following:
// Create a context with a default value of false
let DialogContext = React.createContext()
function MainComponent () {
...
return (<DialogContext.Provider value={{ setDialogOpen: (open) => setShowDialog(open) }}>
{showDialog && (
<Dialog />
)}
<Route ... />
<Route ... />
</ DialogContext.Provider>)
}
function Home ({openDialog}: Props) {
let dialogContext= useContext(DialogContext)
return (
<button onclick={() => dialogContext.setDialogOpen(true)}>AddItem</Button>
)
}
function Books ({openDialog}: Props){
let contextValue = useContext(DialogContext)
return (
<button onClick={() => dialogContext.setDialogOpen(true)}>AddBooks</Button>
)
}
function MessageDialog({hideDialog}: Props) {
let dialogContext= useContext(DialogContext)
return (
<button onClick={() => dialogContext.setDialogOpen(false)}>hide</button>
)
}

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