React Router Navbar depending on route - javascript

I am trying to style a Navbar Link depending on the current path in my React App, if the path is /create or /add it should change it's styling. Here is what I have so far in my header component:
<div
id="createLink"
className={this.state.createClassName}
onClick={() => this.handleModalToggle()}
>
CREATE
</div>
handleActiveLink= () => {
let path = this.props.location.pathname
if (path === "/add" | path === "/create") {
this.setState({createClassName: "nav-link-active"})
} else {
this.setState({ createClassName: "nav-link" })
}
};
componentDidMount() {
this.handleActiveLink()
}
This works but only after I refresh the page which makes sense but it's not what I want. So I am looking for a way to change the className before even rendered and get the path first (I am using withRouter from react-router-dom)

Issue appears to be you only check the path when the component mounts and not when it updates. You should also check in componentDidUpdate
handleActiveLink= () => {
let path = this.props.location.pathname;
if (path === "/add" || path === "/create") {
this.setState({createClassName: "nav-link-active"});
} else {
this.setState({ createClassName: "nav-link" });
}
};
componentDidMount() {
this.handleActiveLink();
}
componentDidUpdate() {
this.handleActiveLink();
}
In this case I instead recommend not storing such transient data in state, and simply derive it from the props and set as a className in the render function (or wherever you render it). This way it's computed each render when the UI is going to be updated by something and will always be up-to-date (i.e. you won't need to worry about lifecycle functions).
render() {
const { location: { pathname } } = this.props;
const linkClass = ["/add", "/create"].includes(pathname)
? "nav-link-active"
: "nav-link";
...
<div
id="createLink"
className={linkClass}
onClick={() => this.handleModalToggle()}
>
CREATE
</div>
...
}

Related

How to navigate to other page when location.state is null

I have a react application where I pass state via react router and access the state using location in the target component/page. It works perfect, however when I close the tab and paste the exact same url to that page in another tab it crashes and says Cannot read properties of null (reading '1'). This is how I am accessing the state:
const { filter, mode } = location?.state[1];
I want to navigate to home page if the location state is null.
I have tried the following but does not seem to work.
if (location.state === null) {
navigate("/");
}
const { filter, mode } = location?.state[1];
Any help will be appreciated
The code is still running after navigate if you don't return
if (location.state===null) {
navigate("/");
return null;
}
const { filter, mode } = location?.state[1];
You will need to split the logic between issuing the imperative navigation action as a side-effect, and returning null from the component so you are not accidentally accessing null or undefined state.
Example:
useEffect(() => {
if (location.state === null) {
navigate("/");
}
}, []);
const { filter, mode } = location?.state[1];
if (!location.state) {
return null;
}
Alternatively you could simply return the Navigate component instead. Just ensure that any and all React hooks are unconditionally called prior to any early returns from the React function body.
Example:
...
if (location.state === null) {
return <Navigate to="/" />;
}
const { filter, mode } = location?.state[1];

How to re-rendering react class component when state is changed

New to both react and javascript and was wondering how do we re-render the component?
I need to make sure that the details (E.g. badge counter) in button component re-render everytime we get a change in facets.
I see some older attempts by other developers to set state but it doesn't seem to be working so i need some guidance on the following.
How to detect when there is a change in facets?
How do we re-render the component everytime we get a change in facets?
class SearchResult extends React.Component {
constructor(props) {
super(props);
this.state = {
_query: props.query,
_facets: props.facets,
_hasLoaded: false,
};
}
render() {
const {
onEntityClick, facets, entity, total, pagingTotal, isFilterSubmitted, loading, query,
} = this.props;
const {
_query, _facets, _hasLoaded,
} = this.state;
if ((_query !== query && query !== '*') && !loading) {
this.setState({
_query: query,
_facets: facets,
});
}
if ((_query === query && query === '*' && !_hasLoaded) && !loading) {
this.setState({
_query: query,
_facets: facets,
_hasLoaded: true,
});
}
return (
<Fragment>
<ButtonComponent
btnTheme="gray"
size="small"
label="Note"
icon="note"
// eslint-disable-next-line no-nested-ternary
badgeCounter={!loading ? entity === 'note' && isFilterSubmitted ? pagingTotal : _facets.note : 0}
disabled={entity === 'note'}
callbackFunc={() => onEntityClick('note')}
/>
</Fragment>
);
}
}
You're making an antipattern in your code:
this.setState() should never be called inside the render() function.
Instead, what you would like to do is check componentDidUpdate():
componentDidUpdate(prevProps) {
if (prevProps.query !== props.query && query !== "*") {
...
}
}
This guarantees a re-rendered so you don't need to worry about "manually" re-rendering.
Also note that props.query as well as other values, don't need to be saved in the component state unless you need a modified copy of it -- that's what didUpdate will do.
You can try componentWillReceiveProps LC method, if there is a change in props and set the state(re-render) of the component acc. to that change.
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
console.log(nextProps.facets);
console.log(this.state._facets);
if (nextProps.facets !== this.state._facets) {
this.setState({
_facets: 'YES'
})
}
}

Close a dropdown when an element within it is clicked

I'm working on a Notification feature in my app (pretty much like Facebook notifications).
When I click a button in the header navigation, the dropdown opens and shows the notification list. The notification has a Link (from react-router) in it.
What I need to do is to close the dropdown whenever a Link is clicked.
Here's roughly the hierarchy I currently have:
Header > Navigation > Button > Dropdown > List > Notification > Link
Since the dropdown functionality is used more that once, I've abstracted its behavior away into a HOC that uses render prop:
export default function withDropDown(ClickableElement) {
return class ClickableDropdown extends PureComponent {
static propTypes = {
children: PropTypes.func.isRequired,
showOnInit: PropTypes.bool,
};
static defaultProps = {
showOnInit: false,
};
state = {
show: !!this.props.showOnInit,
};
domRef = createRef();
componentDidMount() {
document.addEventListener('mousedown', this.handleGlobalClick);
}
toggle = show => {
this.setState({ show });
};
handleClick = () => this.toggle(true);
handleGlobalClick = event => {
if (this.domRef.current && !this.domRef.current.contains(event.target)) {
this.toggle(false);
}
};
render() {
const { children, ...props } = this.props;
return (
<Fragment>
<ClickableElement {...props} onClick={this.handleClick} />
{this.state.show && children(this.domRef)}
</Fragment>
);
}
};
}
The HOC above encloses the Button component, so I have:
const ButtonWithDropdown = withDropdown(Button);
class NotificationsHeaderDropdown extends PureComponent {
static propTypes = {
data: PropTypes.arrayOf(notification),
load: PropTypes.func,
};
static defaultProps = {
data: [],
load: () => {},
};
componentDidMount() {
this.props.load();
}
renderDropdown = ref => (
<Dropdown ref={ref}>
{data.length > 0 && <List items={this.props.data} />}
{data.length === 0 && <EmptyList />}
</Dropdown>
);
render() {
return (
<ButtonWithDropdown count={this.props.data.length}>
{this.renderDropdown}
</ButtonWithDropdown>
);
}
}
List and Notification are both dumb functional components, so I'm not posting their code here. Dropdown is pretty much the same, with the difference it uses ref forwarding.
What I really need is to call that .toggle() method from ClickableDropdown created by the HOC to be called whenever I click on a Link on the list.
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
I'm using redux, but I'm not sure this is the kind of thing I'd put on the store.
Or should I handle this imperatively using the DOM API, by changing the implementation of handleGlobalClick from ClickableDropdown?
Edit:
I'm trying with the imperative approach, so I've changed the handleGlobalClick method:
const DISMISS_KEY = 'dropdown';
function contains(current, element) {
if (!current) {
return false;
}
return current.contains(element);
}
function isDismisser(dismissKey, current, element) {
if (!element || !contains(current, element)) {
return false;
}
const shouldDismiss = element.dataset.dismiss === dismissKey;
return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}
// Then...
handleGlobalClick = event => {
const containsEventTarget = contains(this.domRef.current, event.target);
const shouldDismiss = isDismisser(
DISMISS_KEY,
this.domRef.current,
event.target
);
if (!containsEventTarget || shouldDismiss) {
this.toggle(false);
}
return true;
};
Then I changed the Link to include a data-dismiss property:
<Link
to={url}
data-dismiss="dropdown"
>
...
</Link>
Now the dropdown is closed, but I'm not redirected to the provided url anymore.
I tried to defer the execution of this.toggle(false) using requestAnimationFrame and setTimeout, but it didn't work either.
Solution:
Based on the answer by #streletss bellow, I came up with the following solution:
In order to be as generic as possible, I created a shouldHideOnUpdate prop in the ClickableDropdown dropdown component, whose Hindley-Milner-ish signature is:
shouldHideOnUpdate :: Props curr, Props prev => (curr, prev) -> Boolean
Here's the componentDidUpdate implementation:
componentDidUpdate(prevProps) {
if (this.props.shouldHideOnUpdate(this.props, prevProps)) {
this.toggle(false);
}
}
This way, I didn't need to use the withRouter HOC directly in my withDropdown HOC.
So, I lifted the responsibility of defining the condition for hiding the dropdown to the caller, which is my case is the Navigation component, where I did something like this:
const container = compose(withRouter, withDropdown);
const ButtonWithDropdown = container(Button);
function routeStateHasChanged(currentProps, prevProps) {
return currentProps.location.state !== prevProps.location.state;
}
// ... then
render() {
<ButtonWithDropdown shouldHideOnUpdate={routeStateHasChanged}>
{this.renderDropdown}
</ButtonWithDropdown>
}
It seems you could simply make use of withRouter HOC and check if this.props.location.pathname has changed when componentDidUpdate:
export default function withDropDown(ClickableElement) {
class ClickableDropdown extends Component {
// ...
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.toggle(false);
}
}
// ...
};
return withRouter(ClickableDropdown)
}
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
In the question, you mention that you are using redux.So I assume that you store showOnInit in redux.We don't usually store a function in redux.In toggle function,I think you should dispatch an CHANGE_SHOW action to change the showOnInit in redux, then pass the show data not the function to the children component.Then after reducer dispatch,the react will change “show” automatically.
switch (action.type) {
case CHANGE_SHOW:
return Object.assign({}, state, {
showOnInit: action.text
})
...
default:
return state
}
Link element and data pass
Use the property in Link-to,not data-...Like this:
<Link
to={{
pathname: url,
state:{dismiss:"dropdown"}
}}
/>
And the state property will be found in this.props.location.
give context a little try(not recommend)
It may lead your project to instable and some other problems.(https://reactjs.org/docs/context.html#classcontexttype)
First,define context
const MyContext = React.createContext(defaultValue);
Second,define pass value
<MyContext.Provider value={this.toggle}>
Then,get the value in the nested component
<div value={this.context} />

React conditional rendering with history

I have a parent component which maintains state for three 'form' components that render in sequence. It looks something like this:
<Parent>
{ renderFormBasedOnState() }
</Parent>
FormA renders, then when next is click FormB renders then FormC renders, all in the parent.
Previously I was using a React Router to do this, but the problem is, I don't want the user to be able to bookmark /formb or /formc, as that would be an invalid state.
I can do this with a switch statement, but then I lose forward / back button browser history ability - and I don't want to basically re-implement react-router in my component. What is the simplest way to go about this?
Haven't tried it for the back of the browser, but it could look something like this:
export default class tmp extends React.Component {
state = {
currentVisibleForm: 'A'
}
onBackButtonEvent = (e) => {
if(this.state.currentVisibleForm !== 'A') {
e.preventDefault();
//Go back to the previous visible form by changing the state
} else {
// Nothing to do
}
}
componentDidMount = () => {
window.onpopstate = this.onBackButtonEvent;
}
render() {
return (
<Parent>
{this.state.currentVisibleForm === 'A' &&
<FormA />
}
{this.state.currentVisibleForm === 'B' &&
<FormB />
}
{this.state.currentVisibleForm === 'C' &&
<FormC />
}
</Parent>
)
}
}
Tell me if it is of any help!
So I was able to get this working with the history api, however it may not be worth the effort to fine tune - I may revert. Managing state in two places is kind of dumb. Note this history object is the same from the application's 'Router' component, and doesn't conflict.
state = {
FormData: {},
action: 'Form_1'
}
componentWillMount() {
this.unlistenHistory = history.listen((location) => {
if (location.state) {
this.setState(() => ({
action: location.state.action
}));
}
});
history.push(undefined, {action: 'FORM_1'});
}
componentWillUnmount() {
this.unlistenHistory();
}
finishForm1 = () => {
const action = 'Form_2';
history.push(undefined, { action });
this.setState((prevState) => ({
// business stuff,
action
}));
};
renderCurrentState() {
switch(this.state.action) {
case 'FORM_1':
return <Form1 />
...
}
}
render() {
return (
<div>
{ this.renderCurrentState() }
</div>
);
}

React DnD - nested component can set props but not parent on drop event

I have two DropTargets, ComponentA and ComponentB arranged in this structure:
<ComponentA
dropEvent={this.handleRootDropEvent}
>
<p>Displayed Categories </p>
{
this.state.categories.map((c, idx) =>
<ComponentB
key={idx}
parentCat={null}
thisCat={c}
level={c.level}
dropEvent={this.handleDropEvent}
/>
)
}
</ComponentA>
And the problem I'm having in particular is with the drop target spec drop function. Both of the DropTargets (for ComponentA and B) have the same drop function defined at the moment:
//ComponentB
const targetSpec = {
drop(props, monitor, component) {
const item = monitor.getItem()
if (props.thisCat.category_id == item.category_id) return
component.setState({ droppedItem: item })
component.setState({ droppedItem: null })
},
canDrop(props, monitor) {
const item = monitor.getItem()
if (props.thisCat.category_id == item.category_id) return false
return true
},
hover(props, monitor, component){
}
}
// Component A
const source = {
drop(props, monitor, component) {
const item = monitor.getItem()
component.setState({ droppedItem: item })
component.setState({ droppedItem: null })
}
}
(I can confirm that both are getting called on drop events, and that item is not null in ComponentA's drop function)
It seems that component.setState({}) actually changes the props of the component it's called on, or at least that's how I'm able to use it in the case of ComponentB:
componentWillReceiveProps = () => {
if (this.props.droppedItem) {
this.props.dropEvent(this.props.droppedItem, this.props.thisCat)
}
}
And that code works, I'm able to trigger a function in the parent component (or this case grandparent) of ComponentB by checking for the presence of those props that are set in the drop function.
However, in the same exact componentWillReceiveProps function definition in ComponentA, this.props.droppedItem is always undefined.
Any idea what I could try to get the props passed successfully? Am I misunderstanding React DnD's component.setState({}) API?

Categories

Resources