In an app I'm currently working on, the authentification has been done like this, to pass user's data into children components (in App.js, I would have rather used useContext to access user data in whatever component) [App.js]:
const RequireAuth = ({ children }) => {
// fetch user data from database, ...
return <>{React.cloneElement(children, { user: {...user, ...userExtraData} })}</>;
};
Then, an example of a Route is specified as [App.js]:
<Route
path="/addgame"
element={
<RequireAuth>
<FormAddGame />
</RequireAuth>
}
/>
However, my current problem is the following:
From a ComponentA, I want to navigate to /addgame (=FormAddGame component, see below) while setting an existingGame prop. Thus, I use [ComponentA.js]:
let navigate = useNavigate()
navigate('addgame', { existingGame: game })
The said FormAddGame component is [FormAddGame.js]:
function FormAddGame({ user }) {
const { existingGame } = useLocation()
console.log(existingGame)
...
}
export default FormAddGame;
However, while I correctly navigate to /addgame, the existingGame prop stays undefined once in FormAddGame while it should be not (as game is not undefined)
Try passing props like this:
navigate("addgame", { state: { existingGame: game } });
and destructure like this:
const { state: { existingGame } = {} } = useLocation();
Related
I added a context that contains a useReducer hook to my Ionic React app. I'm seeing some strange behavior: when I update the context value with a dispatch call, then a consumer component will be updated on the page, but the exact same component on the tab bar does not get updated.
I followed this tutorial.
When I add console.log statements to check whether the components are being reloaded, I see that the component placed in the tab bar (<TabBarCounter>) is not being reloaded even though the context value has changed.
When I add console.log statement to check for re-rendering in my context provider, I see that it doesn't get re-rendered when a dispatch is called, either.
It seems like the context is being updated locally rather than globally. There is a comment in this answer:
You are updating your state correctly using a reducer but it will only
update local component state not the global context state.
That sounds a lot like the problem I am having here.
Here's some code:
MyContext.tsx
export const CountContext = React.createContext<any>({} as {
countState: CountState,
countDispatch: React.Dispatch<CountReducerActions>,
});
interface MyProps {
children: JSX.Element,
}
const reducer = (countState: CountState, action: CountReducerActions) => {
switch (action.type) {
case 'add1': {
countObject.total += 1;
return countObject;
}
default: {
throw new Error();
}
}
};
export const CountContextProvider: React.VFC<MyProps> = ({ children }: MyProps) => {
const [countState, countDispatch] = useReducer(
reducer,
{
total: 0,
},
);
return (
<CountContext.Provider value={{ countState, countDispatch }}>
{children}
</CountContext.Provider>
);
};
how I update the context
const { countState, countDispatch } = useContext(CountContext);
countDispatch({ type: 'add1' });
MyComponentThatDoesNotGetRerendered.tsx
import React, { useContext } from 'react';
import { IonBadge } from '#ionic/react';
import { CountContext } from '../../context/CountContext';
const TabBarCounter: React.VFC = () => {
const [countState] = useContext(CountContext);
return (
<IonBadge>
{countState.total}
</IonBadge>
);
};
export default TabBarCounter;
Router.tsx
<CountContextProvider>
<IonReactRouter>
<AppTabBar>
<IonRouterOutlet>
<Route exact path={myPageRoute}>
<MyPage />
</Route>
<Route>
<PageError404 />
</Route>
</IonRouterOutlet>
</AppTabBar>
</IonReactRouter>
</CountContextProvider>
AppTabBar.tsx
const AppTabBar: React.VFC<MyProps> = ({ children }: MyProps) => {
const [userObject] = useContext(UserContext);
return (
<IonTabs>
{children}
<IonTabBar slot="bottom" id="appTabBar">
<IonTabButton tab="tab-settings" href={routeTabSettings}>
<IonLabel>Settings</IonLabel>
</IonTabButton>
<IonTabButton
tab="tab-count"
href={routeTabCount}
>
<TabBarReviewCounter />
<IonLabel>Count</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
In this case, when the context is updated in <MyPage>, the <TabBarCounter> that is inside <AppTabBar> does not get updated, but the <TabBarCounter> inside <MyPage> does get updated.
How do I update the context correctly using useReducer() so that when I update the context value, all the consumers of that context get updated?
Take a look at your reducer. Instead of modifying state in immutable way you simply overwrite property without creating new reference, therefore context value never updates.
Some components may 'see' this change when they get rerendered because of some reason - local state change, prop change etc. They will reach context, look into provided object and see new value.
To fix it use spread operator to create new objects with keys from previous state and updated total property.
case 'add1': {
return {
...countObject,
total: countObject.total + 1,
};
}
I'm using React and React Router. I have all my data fetching and routes defined in App.js.
I'm clicking the button in a nested child component <ChildOfChild /> which refreshes my data when clicking on a button (passed a function down with Context API) with a fetch request happening in my top component App.js (I have a console.log there so it's fetching on that click for sure). But the refreshed state of data never arrives at the <ChildOfChild /> component. Instead, it refreshes the old state. What am I doing wrong. And how can I ensure my state within <Link>is refreshing on state update.
I expect the item.name value to be updated on button click.
App component
has all the routes and data fetching
uses Reacts Context API, which I use to pass my fetching to child components
below the basic shape of the App component.
import React, {useEffect, useState} from "react";
export const FetchContext = React.createContext();
export const DataContext = React.createContext();
const App = () => {
const [data, setData] = useState([false, "idle", [], null]);
useEffect(() => {
fetchData()
}, []);
const fetchData = async () => {
setData([true, "fetching", [], null]);
try {
const res = await axios.get(
`${process.env.REACT_APP_API}/api/sample/`,
{
headers: { Authorization: `AUTHTOKEN` },
}
);
console.log("APP.js - FETCH DATA", res.data)
setData([false, "fetched", res.data, null]);
} catch (err) {
setData([false, "fetched", [], err]);
}
};
return (
<Router>
<DataContext.Provider value={data}>
<FetchContext.Provider value={fetchData}>
<Switch>
<Route exact path="/sample-page/" component={Child} />
<Route exact path="/sample-page/:id" component={ChildOfChild} />
</Switch>
</FetchContext.Provider>
</DataContext.Provider>
</Router>
)
}
Child component
import { DataContext } from "../App";
const Child = () => {
const [isDataLoading, dataStatus, data, dataFetchError] = useContext(DataContext);
const [projectsData, setProjectsData] = useState([]);
{
data.map((item) => (
<Link
to={{
pathname: `/sampe-page/${item.id}`,
state: { item: item },
}}
>
{item.name}
</Link>
));
}
Child of Child component
import { FetchContext } from "../App";
const ChildOfChild = (props) => {
const getData = useContext(FetchContext);
const [item, setItem] = useState({});
const [isItemLoaded, setIsItemLoaded] = useState(false);
useEffect(() => {
if (props.location.state.item) {
setItem(props.location.state.item);
setIsItemLoaded(true);
}
}, [props]);
return (
<div>
<button onClick={() => getData()}Refresh Data</button>
<div>{item.name}</div>
</div>
)
}
Issue
The specific data item that ChildOfChild renders is only sent via the route transition from "/sample-page/" to "/sample-page/:id" and ChildOfChild caches a copy of it in local state. Updating the data state in the DataContext won't update the localized copy held by ChildOfChild.
Suggestion
Since you are already rendering ChildOfChild on a path that uniquely identifies it, (recall that Child PUSHed to "/sample-page/${item.id}") you can use this id of the route to access the specific data item from the DataContext. There's no need to also send the entire data item in route state.
Child
Just link to the new page by item id.
<Link to={`/sampe-page/${item.id}`}>{item.name}</Link>
ChildOfChild
Add the DataContext to the component via useContext hook.
Use props.match to access the route's id match param.
import { FetchContext } from "../App";
import { DataContext } from "../App";
const ChildOfChild = (props) => {
const getData = useContext(FetchContext);
const [,, data ] = useContext(DataContext);
const [item, setItem] = useState({});
const [isItemLoaded, setIsItemLoaded] = useState(false);
useEffect(() => {
const { match: { params: { id } } } = props;
if (id) {
setItem(data.find(item => item.id === id));
setIsItemLoaded(true);
}
}, [data, props]);
return (
<div>
<button onClick={getData}Refresh Data<button />
<div>{item?.name}<div>
</div>
)
}
The useEffect will ensure that when either, or both, the data from the context or the props update that the item state will be updated with the latest data and id param.
Just a side-note about using the Switch component, route path order and specificity matter. The Switch will match and render the first component that matched the path. You will want to order your more specific paths before less specific paths. This also allows you to not need to add the exact prop to every Route. Now the Switch can attempt to match the more specific path "/sample-page/123" before the less specific path "/sample-page".
<Router>
<DataContext.Provider value={data}>
<FetchContext.Provider value={fetchData}>
<Switch>
<Route path="/sample-page/:id" component={ChildOfChild} />
<Route path="/sample-page/" component={Child} />
</Switch>
</FetchContext.Provider>
</DataContext.Provider>
</Router>
I've just rewrote your code here, I've used randomuser.me/api to fetch data
Take a look here, it has small typo errors but looks ok here
https://codesandbox.io/s/modest-paper-nde5c?file=/src/Child.js
In my app, I have a list of university departments. When you click a specific department, you are taken to the department landing page (/department/:deptId). I am using React Router's useParams hook to get the department id from the URL and then find that specific department object from my array of departments passed down as props.
This works fine when navigating from the list of departments to the individual department page, but if I refresh the page, I get the following error: Uncaught TypeError: can't access property "Name", dept is undefined
My code is below:
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
const Department = props => {
const { id } = useParams();
const [dept, setDept] = useState({});
useEffect(() => {
const unit = props.depts.find(item => item.Id === Number(id));
setDept(unit);
}, [id]);
return (
<div>
<h1>{dept.Name}</h1>
</div>
);
};
export default Department;
I'm not sure why this happens. My understanding is that the props should remain the same, and the useEffect should run when the page is refreshed. Any idea what I'm missing?
More code below:
The depts array is passed as props from the App component, which is getting it from an axios call in a Context API component.
import { UnitsContext } from './contexts/UnitsContext';
function App() {
const { units } = useContext(UnitsContext);
return (
<>
<Navigation />
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/people" component={Persons} />
<Route exact path="/department/:id">
<Department depts={units} />
</Route>
</Switch>
</>
);
}
// Context Component. Provider wraps `index.js`
export const UnitsContext = createContext();
export const UnitsContextProvider = props => {
const url = 'http://localhost:5000';
const [units, setUnits] = useState([]);
useEffect(() => {
axios
.get(`${url}/api/units`)
.then(res => {
setUnits(res.data);
})
.catch(err => {
console.log(err);
});
}, []);
return (
<UnitsContext.Provider value={{ units }}>
{props.children}
</UnitsContext.Provider>
);
};
the problem is most probably with this,
useEffect(() => {
const unit = props.depts.find(item => item.Id === Number(id));
setDept(unit); // <<
}, [id]);
Nothing else in ur code sets State except setDept(unit);
So, My best guess is props.depth find matches nothing and returns null. Thats why dept.Name results with the error
From MDN,
The value of the first element in the array that satisfies the provided testing function. Otherwise, undefined is returned
I am attempting to build a component using date from a graphql query but cannot see how dynamically supply a variable if it is not available in props when my component is created. eg. I have:-
class UsageComponent extends Component {
render() {
const params = this.queryStringParse(window.location.href);
const queryStringImageId = params.id;
const imageDetailsResults = this.props.getImageDetails.imageByImageId
(etc...)
}
}
const GET_IMAGE_DETAILS_QUERY = gql`
query getImageDetails($imageId: Int!){
imageByImageId(imageId:$imageId) {
filename
originalFilename
width
height
description
(etc...)
}
}
`;
export default compose(
graphql(GET_IMAGE_DETAILS_QUERY, {name: "getImageDetails", options: props => { return { variables: { imageId: 123456 }}; }}),
graphql(GET_PUBLICATIONS_QUERY, {name: "getPublications"})
)(ImageBrowser);
Which works fine. i.e. imageDetailsResults is populated and I can use the data subsequently in the render function.
However I would like to be able to replace that hard set "imageId: 123456" in the composed graphql with the queryStringImageId value I get in the component render.
I don't think I am able to set this a props value as I'm coming to this component page from a redirected url:-
<Switch>
<AuthRoute path="/image" component={ImageBrowser} token={token} />
</Switch>
Any suggestions would be much appreciated!
You're probably looking for a method to use urls like /image/123456.
With react-router-dom you can define route with param:
<AuthRoute path="/image/:id" component={ImageBrowser} token={token} />
This way component gets router's prop passed in, match.params.id in this case ... and we're only one step away from final param passing:
export default compose(
graphql(GET_IMAGE_DETAILS_QUERY, {
name: "getImageDetails",
options: props => {
return { variables: { imageId: props.match.params.id }};
}}),
graphql(GET_PUBLICATIONS_QUERY, {name: "getPublications"})
)(ImageBrowser);
so I have a redux store with has this basic structure:
{
user: {
id: 1,
currentCompanyId: 2,
}
companyDetails: {
id: 2,
name: 'Joes Company'
},
otherCompanies: [2,3,4],
}
I have a parent page which in the header has a dropdown that allows the user to link / switch to another company.
The parent component needs to know which company is selected so it can show the name in the heading.
The child component displays the details of the current company.
There will be various types of companies and the url's / sections will be different for each type. So in the child component I was trying to set the user's company and then load the details of that company.
The child component doesn't need to directly reference the user or current company.
So what I was doing was in the child component I would listen in willmount and willreceiveprops for a change to the url, then fire an action to update the user company. This will then cause the parent to re render as the user object has changed. Which in turn will create a new / remount the child component. So far this seemed logical.
The issue is that when I have selected company 2 and try to switch to company 3, it will set the company to 3, but then reset it back to 2 again.
I am not sure if this is to do with the URL having not updated or something. I have gone around in circles so much now that I am not sure what the workflow should be anymore.
edit
if I comment out this.loadContractorInfo(contractorId); from ContractorHome.js in componentWillMount() it will work correctly (i.e. the URL stays with the number in the link, vs reverting to the old one. I assume this is to do with redux actions are async, and although I am not doing any network calls it is some race condition between getting data for the contractor page and the contextheader wanting to display / update the current company
edit 2
so to confirm. I have loaded the page at the root of the site, all fine. I select a company / contractor from the dropdown. this loads fine. I go to change that selection to a different contractor from the dropdown. The first component to be hit will be the parent (contextheader), the location prop in nextprops will have updated to have the correct ID in the URL. the method execution at this point will NOT update any thing, no actions are fired. It then hits the child (contractorhome) willreceiveprops method, again the URL in location is good as is the match params. I have commented out all code in willrecieveprops so it does not do anything here either. It will then go back to the parent willreceive props and the location will have gone back to the previous ID in the URL.
app.js snippet:
render() {
return (
<div className="app">
<Router>
<div>
<div className="bodyContent">
<ContextHeader />
<Switch>
{/* Public Routes */}
<Route path="/" exact component={HomePage} />
<Route path="/contractor" component={ContractorRoute} />
<Route path="/building" component={BuildingRoute} />
</Switch>
</div>
<Footer />
</div>
</Router>
</div>
);
}
contextheader:
componentWillReceiveProps(nextProps) {
const { setCompany } = this.props;
const currentInfo = this.props.sharedInfo && this.props.sharedInfo.currentCompany;
const newInfo = nextProps.sharedInfo && nextProps.sharedInfo.currentCompany;
if (newInfo && newInfo.id && (!currentInfo || currentInfo.id !== newInfo.id)) {
setCompany(newInfo.id, newInfo.type);
}
}
render() {
const { user, companies, notifications } = this.props;
/* render things here */
}
);
}
}
const mapStateToProps = state => ({
user: state.user,
sharedInfo: state.sharedInfo,
companies: state.companies,
notifications: state.notifications,
});
const mapDispatchToProps = dispatch => ({
setCompany: (companyId, type) => dispatch(setCurrentCompany(companyId, type)),
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ContextHeader));
contractorRoute:
const ContractorRoute = ({ match }) => (
<Switch>
<Route path={`${match.path}/:contractorId`} component={ContractorHome} />
</Switch>
);
contractorHome
componentWillMount() {
const contractorId = parseInt(this.props.match.params.contractorId, 10);
this.setSharedCompany(contractorId);
this.loadContractorInfo(contractorId);
}
componentWillReceiveProps(nextProps) {
const newContractorId = parseInt(nextProps.match.params.contractorId, 10);
if (this.props.match.params.contractorId !== nextProps.match.params.contractorId) {
this.setSharedCompany(newContractorId);
}
}
setSharedCompany(contractorId) {
const { sharedInfo, setCompany } = this.props;
if (typeof contractorId === 'number') {
if (!sharedInfo || !sharedInfo.currentCompany || !sharedInfo.currentCompany.id || sharedInfo.currentCompany.id !== contractorId) {
setCompany(contractorId);
}
}
}
loadContractorInfo(contractorId) {
const { sharedInfo, getContractorInfo, busy } = this.props;
if (!busy && sharedInfo && sharedInfo.currentCompany && sharedInfo.currentCompany.id === contractorId) {
getContractorInfo(contractorId);
}
}
render() { /*render lots of things here*/};
const mapStateToProps = (state) => {
const selector = state.contractor.details;
return {
sharedInfo: state.sharedInfo,
details: selector.info,
error: selector.request != null ? selector.request.error : null,
busy: selector.request && selector.request.busy,
};
};
const mapDispatchToProps = dispatch => ({
getContractorInfo: contractorId => dispatch(getContractor(contractorId)),
setCompany: contractorId => dispatch(setSharedinfoCurrentCompany(contractorId, 'contractor')),
});
export default connect(mapStateToProps, mapDispatchToProps)(ContractorHome);