useState function passed down through props doesnt change value (In React-Router) - reactjs

Here is my problem is that I need to change the value of the useState in parent component, from the child component(s). but the child components are Routes, so I passed them through like this;
const NurseInterface = () => {
const [wristbandID, setWristbandID] = useState(1);
...
<Switch>
<Route
exact path={`${path}/default-view`}
render={(props)=> (
<NurseDefaultView {...props} setWristbandID={setWristbandID}/>// Alternative way of Routing Components, allowing you to pass props to it
)}
/>
...
</Switch>
...
}
inside the Route is the NurseDefaultView which passes the state to Wristbands:
const NurseDefaultView = ({ setWristbandID }) => {
...
{wristbands ? <Wristbands wristbands={wristbands} setWristbandID={setWristbandID}/> : null}
}
and then Wristbands passes the state down to wristband cards through map()
//React-Spring
const transitions = useTransition(wristbands, wristband => wristband.id, {
from: { opacity: 0, transform:"translate(0px, -50px)" },
enter: { opacity: 1, transform:"translate(0px, 0px)" },
leave: { opacity: 0, transform:"translate(0px, -50px)" },
})
if (wristbands) {
return (
<>
{transitions.map(({item, key, props})=> (
item ? (
<WristbandCard key={parseInt(item.id)}
props={props}
nurseView={true}
setWristbandID={setWristbandID}
wristband={item}/> ) : null
))}
</>
)} else return <div>No Wristbands</div>
and WristbandCard looks this this:
const WristbandCard = ({ wristband, setWristbandID, setShowDetail, nurseView, props }) => {
...
const handleClick = () => {
if (nurseView) {
console.log(wristband.id);
setWristbandID(wristband.id);
} else {
console.log("SetShowDetail")
setShowDetail(true);
setWristbandID(wristband.id);
}
}
...
if (nurseView) return (
<Link className="right" to={`patient-cart`}>
<animated.article style={props} onClick={handleClick} className="card">
<header>
{battery(data)}
<img alt="Wifi Icon by Google Inc (placeholder)" src={Connection}/>
</header>
<h1>{wristband.name}</h1>
<div><img alt="transfer" src={Arrow}/><h3>{description(wristband, nurseView)}</h3></div>
<footer><img alt="transfer" src={Transfer}/></footer>
</animated.article>
</Link>
); else return (
<animated.figure style={props} onClick={handleClick} className={wristband.connected ? "card": "card red"}>
<header>
{battery(data)}
<img alt="Wifi Icon by Google Inc (placeholder)" src={Wifi}/>
</header>
<h3>{description(wristband)}</h3>
<h2>{wristband.name}</h2>
</animated.figure>
)
}
but it doesnt work. And ive console logged the value of it set at the top level inside the Route, and the value. But in the parent, it doesn't work.
Does anyone have any examples of this working in react-router? I havent seen any sadly.
This makes me think that maybe Routes cant handle Hooks as props and I need to implement Redux for this, but i'm hoping that there is maybe somewhere I went wrong? because this is the only case where I need something like this.
I would be grateful for any help. Thanks.

You can use context with a context hook to update a parent's state (wristband id) in a nested child component (setWristbandId).

Related

'Maximum update depth exceeded' error when trying to use custom function passed as props in 'onClick' of button

I am trying to use a pass a function as props declared in App.js to handle a state variable in App.js in order to add items to a cart component but get this error as soon as I add the function to the onClick field of the "Add" button in my product component(at the end of post):
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
My App.js looks like this:
const App = () => {
const [cartItems, setCartItems] = useState([]);
const handleAddProduct = (product) => {
//some logic to add product to cartItems list here
}
return(
<Box className="App">
<AppRoutes handleAddProduct={handleAddProduct} cartItems={cartItems}/>
</Box>
);
}
Im passing the function and the state variable as props to my routes component so I can access it in my Products page:
const AppRoutes = ({ handleAddProduct, cartItems }) => {
return (
<Routes>
<Route exact path="/alldrip" element={<ProductsPage handleAddProduct={handleAddProduct} />} />
</Routes>
)}
And in my products page the function gets passed as props again to another component:
const ProductsPage = ({ handleAddProduct }) => {
return (
<AllProducts handleAddProduct={handleAddProduct} />
)}
And then I pass the function one last time in AllProducts to an individual Product component: ( I seperated the components this way so it is easier for me to read)
const AllProducts = ({ handleAddProduct }) => {
return (
{products.map(product => {
return (
<Product handleAddProduct={handleAddProduct} product={product} />
)
})}
)}
The products load fine but the app crashes with the error as soon as I add the function to the "Onclick" of the add to cart button:
const Product = ({ handleAddProduct, product }) => {
return (
<Heading>{product.name}</Heading>
<Text >{product.material}</Text>
<Text>{product.price} </Text>
<Button onClick={handleAddProduct(product)} >Add to Cart</Button>
)}
If I remove the function the app stays alive !
I'm not understanding why the error states setState is getting called repeatedly.
onClick={handleAddProduct(product)}
This should probably only be
onClick={() => handleAddProduct(product)}
otherwise you're calling the handleAddProduct method on render directly and not on click.
You call your handleAddProduct directly in your jsx that re-renders the component that directly call handleAddProduct and so on ...
You can try
onClick={() => handleAddProduct(product)}
A better approach is to avoid anonymous functions so in your Product component
const Product = ({ handleAddProduct, product }) => {
const onAddProduct = (product) => {
handleAddProduct(product)
}
return (
...
<Button onClick={onAddProduct}>Add to Cart</Button>
)
)}

React hooks : 'Cannot read property 'push' of undefined'

I'm trying to redirect my homepage to "/call" page based on a redux state. I can go to that component by typing the url manually but cant do it with a function. I tried "Redirect to", "history.push" but none of them worked for me. I cant solve the problem. Here is my code;
const Phone = ({ hidden, photoOpened, inCall }) => {
const dispatch = useDispatch(getContacts());
let history = useHistory();
useEffect(() => {
if (inCall.inCall) {
history.push('/call')
}
}, [inCall]);
useEffect(() => {
dispatch(getContacts());
}, [])
return (
<div hidden={process.env.NODE_ENV === 'development' ? !hidden : hidden} className={photoOpened ? "phone-container-rotate" : "phone-container"}>
<div className="coque" />
<Suspense fallback={<div className="animated fadeIn pt-1 text-center">Loading...</div>}>
<HashRouter basename="/phone">
<div
className="phone-content"
style={{ backgroundImage: `url(${background})` }}
>
<HeaderBar />
<BannerNotifications />
<Switch>
{routes.map((route, idx) => {
return route.component ? (
<Route
key={idx}
path={route.path}
exact={route.exact}
render={props => <route.component {...props} />}
/>
) : null;
})}
</Switch>
</div>
<Route component={BottomPhoneNavigator} />
</HashRouter>
</Suspense>
</div>
);
};
You could try and test for history existence of the history in your effect, also add it to dependency list
useEffect(() => {
if (history && inCall.inCall) {
history.push('/call')
}
}, [inCall, history]);
And important thing, your component using this hook must be within the Router, I see you'\re using HashRouter but as child of component using the hook.
Also if you're stuck to this structure, why wont you try to use Redirect within the Switch? This could work with some smart test so you wont end up in a loop:)
To use history your Phone component should be inside router component

Handle children.props

so, in ParentComponent I have
<Component dropDownContent={<DropDownContent content={array} onSelect={handleSelect} />} />
my DropDownContent looks something like this
return (<ul>
{content.map((item) =>
{ return <li><button onClick={()=> onSelect(array.id)}>{array.name}</button></li>}
)}
</ul>)
Can I some how do something with the onSelect inside Component even if I add DropDownContent as a prop to Component?
Thanks :)
What I understand from your question is that you want to pass a function from the parent component to the child component. And when a local function inside child component is clicked, you want to call that passed function. if yes, then this is your solution:
Note: I do not know exactly what code you wrote and what your component consists of. So I will give you the answer by giving a simple example to fully understand the solution.
In your parent component:
export const ParentComponent = props => {
const handleSelect = () => console.log(`do something here`);
return (
<Component
dropDownContent={
<DropDownContent
content={array}
onSelect={() => handleSelect()}
/>}
/>
)
}
And in your child component you need to receive the passed function like below:
export const ChildComponent = props => {
const handlePassedFunction = () => props.onSelect?.();
return (<ul>
{content.map((item) => {
return <li>
<button onClick={() => handlePassedFunction(array.id)}>{array.name}</button>
</li>
}
)}
</ul>)
}

React wrapper component (HOC) does not re-render child component when it's props are changing

My wrapper component has this signature
const withReplacement = <P extends object>(Component: React.ComponentType<P>) =>
(props: P & WithReplacementProps) => {...}
Btw, full example is here https://codepen.io/xitroff/pen/BaKQNed
It's getting original content from argument component's props
interface WithReplacementProps {
getContent(): string;
}
and then call setContent function on button click.
const { getContent, ...rest } = props;
const [ content, setContent ] = useState(getContent());
I expect that content will be replaced everywhere (1st and 2nd section below).
Here's the part of render function
return (
<>
<div>
<h4>content from child</h4>
<Component
content={content}
ReplaceButton={ReplaceButton}
{...rest as P}
/>
<hr/>
</div>
<div>
<h4>content from wrapper</h4>
<Hello
content={content}
ReplaceButton={ReplaceButton}
/>
<hr/>
</div>
</>
);
Hello component is straightforward
<div>
<p>{content}</p>
<div>
{ReplaceButton}
</div>
</div>
and that's how wrapped is being made
const HelloWithReplacement = withReplacement(Hello);
But the problem is that content is being replaced only in 2nd part. 1st remains untouched.
In the main App component I also replace the content after 20 sec from loading.
const [ content, setContent ] = useState( 'original content');
useEffect(() => {
setTimeout(() => {
setContent('...too late! replaced from main component');
}, 10000);
}, []);
...when I call my wrapped component like this
return (
<div className="App">
<HelloWithReplacement
content={content}
getContent={() => content}
/>
</div>
);
And it also has the issue - 1st part is updating, 2nd part does not.
It looks like you are overriding the withReplacement internal state with the external state of the App
<HelloWithReplacement
content={content} // Remove this to stop overriding it
getContent={() => content}
/>
Anyway it looks weird to use two different states, it is better to manage your app state in only one place

Keeping Material UI tabs and React Router in sync

Is there a non hacky way to keep Material UI tabs and React router in sync?
Basically, I want to change the URL when the user clicks on a tab [1] and the tabs should change automatically when the user navigates to a different page with a non-tab link or button, and of course on direct access [2] and page refresh too.
Also, it would be nice to have the react router's non exact feature too, so the /foo tab should be active both for /foo and /foo/bar/1.
[1] Other SO answers recommend using the history api directly, is that a good practice with react-router?
[2] I'm not sure what it's called, I meant when the user loads for example /foo directly instead of loading / and then navigating to /foo by a tab or link
Edit:
I created a wrapper component which does the job, but with a few problems:
class CustomTabs extends React.PureComponent {
constructor() {
super();
this.state = {
activeTab: 0
}
}
setActiveTab(id) {
this.setState({
activeTab: id
});
return null;
}
render() {
return (
<div>
{this.props.children.map((tab,index) => {
return (
<Route
key={index}
path={tab.props.path||"/"}
exact={tab.props.exact||false}
render={() => this.setActiveTab(index)}
/>
);
})}
<Tabs
style={{height: '64px'}}
contentContainerStyle={{height: '100%'}}
tabItemContainerStyle={{height: '100%'}}
value={this.state.activeTab}
>
{this.props.children.map((tab,index) => {
return (
<Tab
key={index}
value={index}
label={tab.props.label||""}
style={{paddingLeft: '10px', paddingRight: '10px', height: '64px'}}
onActive={() => {
this.props.history.push(tab.props.path||"/")
}}
/>
);
})}
</Tabs>
</div>
);
}
}
And I'm using it like this:
<AppBar title="Title" showMenuIconButton={false}>
<CustomTabs history={this.props.history}>
<Tab label="Home" path="/" exact/>
<Tab label="Foo" path="/foo"/>
<Tab label="Bar" path="/bar"/>
</CustomTabs>
</AppBar>
But:
I get this warning in my console:
Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
I think it's because I set the state immediately after render() is called - because of Route.render, but I have no idea how to solve this.
The tab changing animations are lost: http://www.material-ui.com/#/components/tabs
Edit #2
I finally solved everything, but in a bit hacky way.
class CustomTabsImpl extends PureComponent {
constructor() {
super();
this.state = {
activeTab: 0
}
}
componentWillMount() {
this.state.activeTab = this.pathToTab(); // eslint-disable-line react/no-direct-mutation-state
}
componentWillUpdate() {
setTimeout(() => {
let newTab = this.pathToTab();
this.setState({
activeTab: newTab
});
}, 1);
}
pathToTab() {
let newTab = 0;
this.props.children.forEach((tab,index) => {
let match = matchPath(this.props.location.pathname, {
path: tab.props.path || "/",
exact: tab.props.exact || false
});
if(match) {
newTab = index;
}
});
return newTab;
}
changeHandler(id, event, tab) {
this.props.history.push(tab.props['data-path'] || "/");
this.setState({
activeTab: id
});
}
render() {
return (
<div>
<Tabs
style={{height: '64px'}}
contentContainerStyle={{height: '100%'}}
tabItemContainerStyle={{height: '100%'}}
onChange={(id,event,tab) => this.changeHandler(id,event,tab)}
value={this.state.activeTab}
>
{this.props.children.map((tab,index) => {
return (
<Tab
key={index}
value={index}
label={tab.props.label||""}
data-path={tab.props.path||"/"}
style={{height: '64px', width: '100px'}}
/>
);
})}
</Tabs>
</div>
);
}
}
const CustomTabs = withRouter(CustomTabsImpl);
Firstly, thanks for replying to your very question.
I have approached this question differently, I decided to post here for the community appreciation.
My reasoning here was: "It would be simpler if I could tell the Tab instead the Tabs component about which one is active."
Accomplishing that is quite trivial, one can do that by setting a known fixed value to the Tabs component and assign that very value to whatever tab is supposed to be active.
This solution requires that the component hosting the tabs has access to the props such as location and match from react-router as follows
Firstly, we create a function that factory that removes bloated code from the render method. Here were are setting the fixed Tabs value to the Tab if the desired route matches, other wise I'm just throwing an arbitrary constant such as Infinity.
const mountTabValueFactory = (location, tabId) => (route) => !!matchPath(location.pathname, { path: route, exact: true }) ? tabId : Infinity;
After that, all you need is to plug the info to your render function.
render() {
const {location, match} = this.props;
const tabId = 'myTabId';
const getTabValue = mountTabValueFactory(location, tabId);
return (
<Tabs value={tabId}>
<Tab
value={getTabValue('/route/:id')}
label="tab1"
onClick={() => history.push(`${match.url}`)}/>
<Tab
value={getTabValue('/route/:id/sub-route')}
label="tab2"
onClick={() => history.push(`${match.url}/sub-route`)}
/>
</Tabs>
)
}
You can use react routers NavLink component
import { NavLink } from 'react-router-dom';
<NavLink
activeClassName="active"
to="/foo"
>Tab 1</NavLink>
When /foo is the route then the active class will be added to this link. NavLink also has an isActive prop that can be passed a function to further customize the functionality which determines whether or not the link is active.
https://reacttraining.com/react-router/web/api/NavLink

Resources