re render specific component on each route change - javascript

I've been looking around to find a solution and couldn't find anything.
All results that came from searching this was on how to avoid other components re rendering .
Basically as the title says I am trying to make a component which is outside the routes to re render on each route change .
This component returning a notification if history object has a specific state .
Right now this component work as it should, the only problem is that it's only rendered once so it wont search to see if history object has been changed and the notification wont happend .
This is my App :
const App = () => {
return(
<Router history={history}>
<NotificationComponent>
{searchSystemNotification()}
</NotificationComponent>
<img id="background-shape" src={shape} alt="" />
<Header />
<Routes />
<Footer />
</Router>
)
}
This is the function that runs inside the notification component to see if history object has the specific state it's looking for :
const searchSystemNotification = () => {
if(typeof history.location.state !== 'undefined'){
if(history.location.state.hasOwnProperty('message')){
let systemNotification = history.location.state.message;
// Delete message after showing it
let state = history.location.state;
delete state.message;
history.replace({ ...history.location, state });
return(
<Message type={systemNotification.type} text={systemNotification.text} />
)
}
}
}
Basically all i need is to re render "NotificationComponent" on each route change to ofcourse return the "Message" component if the state exists .

useHistory
You can use the useHistory hook anywhere inside your Router to subscribe to changes in its history.
I don't know what your NotificationComponent looks like. One possibility is to call useHistory in the NotificationComponent and define its child as a function of history.
Based on what I'm seeing here, the most straight-forward thing to do is to refactor searchSystemNotification() into a function component and call useHistory there. The component will either render a Message or null.
const SearchSystemNotification = () => {
const history = useHistory();
if(typeof history.location.state === 'object'){
if(history.location.state.hasOwnProperty('message')){
// let's use destructuring instead of `delete`
const {message, ...rest} = {history.location.state};
// pass all values other than message
history.replace({ ...history.location, state: rest });
return(
<Message type={message.type} text={message.text} />
)
}
}
// if we aren't rendering a message then return null
return null;
}
Removing Message
We have a big problem with the above code. Our component will re-render with the new value of history every time that history changes. But inside the hook we are changing history by removing the message from the state. So the message will show for a moment and then immediately disappear as our component re-renders in response to the changes that it made.
We want to either A) just keep the message in the history or B) remove the message only in response to user action, like clicking an "x" on the message.
For B), try this:
const SearchSystemNotification = () => {
const history = useHistory();
if(typeof history.location.state === 'object'){
if(history.location.state.hasOwnProperty('message')){
// let's use destructuring instead of `delete`
const {message, ...rest} = history.location.state;
const onClickClose = () => {
history.replace({ ...history.location, state: rest });
}
return(
<Message type={message.type} text={message.text} onClickClose={onClickClose} />
)
}
}
// if we aren't rendering a message then return null
return null;
}
Edit: useLocation
It turns out we need to use useLocation in order to reload on location changes. We can combine this with useHistory to handle closing the message like this:
const SearchSystemNotification = () => {
const history = useHistory();
const location = useLocation();
if (typeof location.state === "object") {
if (location.state.hasOwnProperty("message")) {
// let's use destructuring instead of `delete`
const { message, ...rest } = location.state;
const onClickClose = () => {
history.replace({ ...history.location, state: rest });
};
return (
<Message
type={message.type}
text={message.text}
onClickClose={onClickClose}
/>
);
}
}
// if we aren't rendering a message then return null
return null;
};
Alternatively, we can avoid making changes to history by keeping the message in the history and having the Message component render conditionally based on its own internal state. We can hide it based on a user click of an "X" or based on a timeout.
const SearchSystemNotification = () => {
const location = useLocation();
if (typeof location.state === "object") {
if (location.state.hasOwnProperty("message")) {
return (
<Message
{...location.state.message}
/>
);
}
}
// if we aren't rendering a message then return null
return null;
};

Related

Redirect in React router dom not redirecting to the given path

I have a make payment function which is called on a button click and if the user is not signed in i want to redirect that user to the signin component .
This is my code of the function.
const PaymentMethod = ()=>{
if(!isAuthenticated())
{
toast.error('Please signin to continue');
history.push('/signin') //works properly
// return <Redirect to = '/signin' /> // not working properly
}
}
// isAuthenticated is a boolean function that look whether the user is signed in or not
Try
return <Redirect to = '/signin' />
Because you have to render the Redirect to make an effect
If PaymentMethod a React component, then the history.push needs to be called in a useEffect hook with proper dependency or in a callback function as an intentional side-effect, not as an unintentional side-effect in the render body. The Redirect component would need to be returned from the component in order to have any effect.
useEffect Example to issue imperative redirect:
const PaymentMethod = () => {
const history = useHistory();
React.useEffect(() => {
if (!isAuthenticated()) {
toast.error('Please sign in to continue');
history.replace('/signin');
}
}, [history, toast]);
...
};
<Redirect> Example to issue declarative redirect:
const PaymentMethod = () => {
React.useEffect(() => {
if (!isAuthenticated()) {
toast.error('Please sign in to continue'); // <-- still a side-effect!!
}
}, [toast]);
if (!isAuthenticated()) {
return <Redirect to='/signin' />;
}
... regular JSX return ...
};
If PaymentMethod is the callback function then it must issue the imperative redirect via the history.replace function, returning JSX from an asynchronously called callback doesn't render it to the DOM. You should also not name the callback handler like it's a React function component. This is to remove any confusion.
const history = useHistory();
...
const paymentMethodHandler = () => {
if (!isAuthenticated()) {
toast.error('Please sign in to continue');
history.replace('/signin');
}
};
...
It looks like <Redirect to = '/signin' /> is a component and not a JS that can be called. Components need to be rendered in order for their JS to run.
If you for some reason want to specifically use redirect component you will have to put it into DOM on click.
you can simply use useNavigate() hook.
example
const PaymentMethod = () =>{
if(!isAuthenticated())
{
toast.error('Please signin to continue');
history.push('/signin')
useNavigate('/signin')
}
}

react testing library ternary operator, finding right component

I'm beginner with React testing, learning by coding, here i have a component 'cam.tsx'
i want to test it, when i want to test Add function it goes straight like this, but when i want to test Update function it still shows Add function in my test, how to test both of them ?
Add and Update functions are forms where user can fill.
describe("Testing component ", () => {
const Camera = (): RenderResult =>
render(
<Provider store={store}>
<Cam
}}
/>{" "}
</Provider>
);
test("Cam", () => {
Camera();
const name = screen.queryByTestId(/^AddName/i);
});
});
cam.tsx:
const ADD = "ADD";
let [state, setState] = useState<State>({mode: ADD });
if (props.mode) {
state.mode = props.mode;
}
const option = state.mode;
return (
<React.Fragment>
<div data-testid="header">
{option == ADD ? Add() : <></>}
{option == UPDATE ? Update() : <></>}
</div>
</React.Fragment>
Basically cam.tsx is a component which has two forms one for updating camera and another for adding new camera.When user clicks add/update icon then cam component gets 'mode' via props ' state.mode = props.mode '
English is not my mother language, so could be mistakes
Here is how to test a component that conditionally renders components from state and can be updated via props.
import {render, screen} from '#testing-library/react';
import {Cam} from './Cam';
test('renders add by default', () => {
render(<Cam/>);
expect(screen.getByTestId('addForm'))
.toBeInTheDocument();
expect(screen.queryByTestId('updateForm'))
.not.toBeInTheDocument();
});
test('renders edit by passing props', () => {
const {rerender} = render(<Cam mode={undefined}/>);
rerender(<Cam mode={'UPDATE'} />)
expect(screen.getByTestId('updateForm'))
.toBeInTheDocument();
expect(screen.queryByTestId('addForm'))
.not.toBeInTheDocument();
});
However, it is known in the React community that updating state via props is usually an anti-pattern. This is because you now have two sources of truth for state and can be easy to have these two states conflicting. You should instead just use props to manage rendering.
If state comes from a parent component, use props.
export function Cam(props) {
const option = props.mode;
return (
<div data-testid="header">
{option === ADD ? Add() : <></>}
{option === UPDATE ? Update() : <></>}
</div>
);
}
If you really want to keep state in the child component even if props are passed in, you should update props in an useEffect hook. Additionally, you should use the setState function rather than setting state manually state.mode = props.mode
Use the useEffect hook to update state via props.
...
const [state, setState] = useState({mode: ADD});
useEffect(() => {
if (props.mode) {
setState({mode: props.mode});
}
}, [props.mode]) <-- checks this value to prevent infinite loop.
const option = state.mode;
return (
...

Why's the button displayed only when refreshing the page?

I want the logout button to show up only when the user's logged in. It kind of works now but it's not working the way it should.
With the code below, the logout button shows up when the user's logged in only if the page's refreshed. That's because upon the <App/> component being loaded, the <Navbar/> component mounted along with it.
But how can I make it so that even if <Navbar/>'s loaded, it can still be possible to manipulate when the button can appear based on if the auth token is not null?
Here's App.js:
const App = () => {
let [logoutButtonFlag, setLogoutButtonFlag] = useState(false);
let authToken = localStorage.getItem('token');
useEffect(() => {
if (authToken !== null) {
setLogoutButtonFlag(true);
}
}, [authToken]);
return (
<>
<Navbar logoutButtonFlag={logoutButtonFlag}/>
</>
);
}
export default App;
Here's Navbar.js:
const Navbar = (props) => {
return (
{!props.logoutButtonFlag ? null : <button className="learn-more">Logout</button>}
);
};
export default Navbar;
you are providing a non-state variable to the list of states that useEffect hook 'listen' to, so it will not run again after you change its value.
const [authToken, setAuthToken] = useState(localStorage.getItem('token'));
and when you update the local storge "token" also update authToken to the same value.
and your useEffect will retrigger on authToken change because now its a state
useEffect(() => {
if (authToken !== null) {
setLogoutButtonFlag(true);
}
}, [authToken]);
The reason way only on refresh it was updating is because the value of the "token" in local storage was changed already.

Too many re-render problems with React

I have some cards in my application that can lead to another pages through clicks. So I have a main component that contains a button like this:
function MainComponent(props) {
.
.
.
const handleClick = (key) => {
history.push("/exampleurl/" + key);
};
Then according to the key passed, I have to make a request that gives me some information required to display it. As default I have my initial state as null, and when it completes the request, it changes to the object I got. But as soon as I click on the card, I get the re-render error.
function MyComponent(props) {
let { key } = useParams();
const [myObject, setMyObject] = React.useState(null)
useEffect(() => {
axios.get('/myendpoint/' + key).then( response => {
let myObject = response.data
setMyObject(myObject)
})
}, [key])
I suppose that the solution is avoiding the key value to update when it changes the state. But i am not finding the solution to this trouble.
Edit: The route that leads to the components:
<Route path="/inbox">
<MainComponent />
</Route>
<Route path="/exampleurl/:key">
<NewComponent />
</Route>
I think the problem is related to the handleClick function.
Every time this method is called, you push a new entry to the history stack. Which analyze your defined routes and render the linked component. In your case, it is the same component, but I am not sure if the router is capable to determine it, therefore I would expect a re-render.
Maybe a solution would be to include another state which is responsible to inform the component of the current obj being displayed on the screen. So key will be responsible only for the route parameter and this new state will be responsible for the internal navigation.
function MyComponent(props) {
let { key } = useParams();
const [myObject, setMyObject] = React.useState(null)
const [displayedObj, setDisplayedObj] = React.useState('');
useEffect(() => {
axios.get('/myendpoint/' + key).then( response => {
let myObject = response.data
setMyObject(myObject)
setDisplayedObj(key)
})
}, [key, displayedObj]) // we listen for displayedObj too
and then in the handleClick we update this new state. This will trigger the useEffect and therefore update the myObject state to the new value:
const handleClick = (key) => {
setDisplayedObj(key);
// This will trigger the useEffect and refresh
// the data displayed without reloading the page
};

Seeking advice: Reason for maximum update depth exceed

I am new to React and GraphQL. Trying to update React state with GraphQL subscription feed but it generates the update depth error.
Here is the simplified code:
import { Subscription } from 'react-apollo';
...
function Comp() {
const [test, setTest] = useState([]);
const Sub = function() {
return (
<Subscription subscription={someStatement}>
{
result => setTest(...test, result.data);
return null;
}
</Subscription>
);
};
const Draw = function() {
return (
<div> { test.map(x => <p>{x}</p>) } </div>
);
};
return (
<div>
<Sub />
<Draw />
<div/>
);
};
export default Comp;
Regular query works fine in the app and the Subscription tag returns usable results, so I believe the problem is on the React side.
I assume the displayed code contains the source of error because commenting out the function "Sub" stops the depth error.
You see what happens is when this part renders
<Subscription subscription={someStatement}>
{
result => setTest(...test, result.data);
return null;
}
</Subscription>
setTest() is called and state is set which causes a re-render, that re-render cause the above block to re-render and setTest() is called again and the loop goes on.
Try to fetch and setTest() in your useEffect() Hook so it does not gets stuck in that re-render loop.
useEffect like
useEffect(() => {
//idk where result obj are you getting from but it is supposed to be
//like this
setTest(...test, result.data);
}, [test] )
Component Like
<Subscription subscription={someStatement} />

Categories