I'm trying to switch from using redux in my new projects to using hooks. My understanding is that I can use a custom hook such as this one to an load it from various components to access the same state similar to how redux would let me access state.
import { useState } from 'react';
export const useSelectedStore = () => {
const [selectedStore, setSelectedStore] = useState();
return [
selectedStore,
setSelectedStore,
];
};
The issue that I'm having is that I have a page with an item where when the user clicks an item I need to set the selected store and then redirect them to the page. Here is the click action within the component:
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Home from './pages/Home/Home';
import Stores from './pages/Stores/Stores';
import StoreDetails from './pages/StoreDetails/StoreDetails';
function App() {
return (
<Router>
<div>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/stores">
<Stores />
</Route>
<Route path="/store-details">
<StoreDetails />
</Route>
</Route>
</Switch>
</div>
</Router>
);
}
export default App;
import React, { useState } from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useSelectedStore } from '../../hooks/useSelectedStore';
function Stores(props) {
const { history } = props;
const [selectedStore, setSelectedStore] = useSelectedStore();
const goToStoreDetails = (index) => {
// Now add our store data
setSelectedStore(storeRequestDetails.stores[index]);
console.log(selectedStore);
history.push('/store-details');
};
The console log outputs undefined because the store hasn't been set yet so the history push happens before the value truly gets set.
I tried to use useEffect like this in the component to pick up on the change and handle the change before redirect but the state is getting reset on the component for the store-details page.
import React, { useEffect, useState } from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useSelectedStore } from '../../hooks/useSelectedStore';
function Stores(props) {
const { history } = props;
const [selectedStore, setSelectedStore] = useSelectedStore();
useEffect(() => {
if (selectedStore) {
console.log(selectedStore);
history.push('/store-details');
}
}, [selectedStore]);
const goToStoreDetails = (index) => {
// Now add our store data
setSelectedStore(storeRequestDetails.stores[index]);
};
The console log here will show the actual selected store details but again once the next page loads like so the state is now undefined again.
import React from 'react';
import { useSelectedStore } from '../../hooks/useSelectedStore';
function StoreDetails() {
const [selectedStore, setSelectedStore] = useSelectedStore();
console.log(selectedStore); // Returns undefined
How do I share the state correctly between these two components?
My understanding is that I can use a custom hook such as this one to an load it from various components to access the same state similar to how redux would let me access state.
The custom hook you showed will cause multiple components to each create their own independent local states. These sorts of custom hooks can be useful for code reuse, but it doesn't let components share data.
If you want to share data between components, the general react approach is to move the state up the tree until it's at the common ancestor of every component that needs it. Then the data can be passed down either through props or through context. If you want the data to be globally available, then you'll move it to the top of the component tree and almost certainly make use of context rather than props.
This is what redux does: You set up the store near the top of the tree, and then it uses context to make it available to descendants. You can do similar things yourself, by using a similar pattern. Somewhere near the top of the tree (perhaps in App, but you could also move it elsewhere), create state, and share it via context:
export const SelectedStoreContext = React.createContext();
function App() {
const value = useState();
return (
<SelectedStoreContext.Provider value={value}>
<Router>
<div>
//etc
</div>
</Router>
</StoreContext.Provider>
);
}
And to use it:
function StoreDetails() {
const [selectedStore, setSelectedStore] = useContext(SelectedStoreContext);
If you want to call this useSelectedStore and slightly hide the fact that context is involved, you can create a custom hook like this:
const useSelectedStore = () => useContext(SelectedStoreContext);
// used like
const [selectedStore, setSelectedStore] = useSelectedStore();
Customs hook do not share any data. They are just compositions of the built-in hooks. But you can still share the store state by using react context:
/* StoreContext.js */
const StoreContext = createContext(null);
export const StoreProvider = ({children}) => {
const storeState = useState();
return (
<StoreContext.Provider value={storeState}>
{children}
</StoreContext.Provider>
);
};
export const useStore = () => useContext(StoreContext);
Usage:
/* App.js */
/* wrap your components using the store in the StoreProvider */
import {StoreProvider} from './StoreContext';
const App = () => (
<StoreProvider>
{/* some children */}
</StoreProvider>
);
and
/* StoreDetails.js */
/* use the useStore hook in your components */
import {useStore} from './StoreContext';
function StoreDetails() {
const [selectedStore, setSelectedStore] = useStore();
// ...
}
Related
I've currently set up the a React context to share the state and the current version looks like the following:
QueryContext.js
import { createContext } from "react";
const queryContext = createContext({
query: "",
setQuery: (value) => {},
});
export default queryContext;
App.js
import React, { useState } from "react";
import QueryContext from "./contexts/QueryContext";
import Header from "./Header";
import Nav from "./Nav";
import Main from "./Main";
import Footer from "./Footer";
const App = () => {
const [query, setQuery] = useState("");
return (
<>
<QueryContext.Provider value={{ query, setQuery }}>
<Header />
</QueryContext.Provider>
<Nav />
<QueryContext.Provider value={{ query, setQuery }}>
<Main />
</QueryContext.Provider>
<Footer />
</>
);
};
export default App;
Since Nav is placed between Header and Main, there was no choice but wrapping both with separate providers.
Should I just use props instead of context like the following?
App.js
import React, { useState } from "react";
import Header from "./Header";
import Nav from "./Nav";
import Main from "./Main";
import Footer from "./Footer";
const App = () => {
const [query, setQuery] = useState("");
return (
<>
<Header value={{ query, setQuery }} />
<Nav />
<Main value={{ query, setQuery }} />
<Footer />
</>
);
};
export default App;
Context is technology which allows developers to pass data through a lot of levels of application tree. The first thing to remember here - if something is global, it should probably be placed in context and accessed from anywhere with level lower. Passing data through several levels using properties will probably be bad practice - code quality will decrease along with performance, probably.
I assume, that in your case it is better to make context (you already did - QueryContext) and place it as higher as allowed:
import React, { useState } from "react";
import QueryContext from "./contexts/QueryContext";
import Header from "./Header";
import Nav from "./Nav";
import Main from "./Main";
import Footer from "./Footer";
const App = () => {
const [query, setQuery] = useState("");
return (
<QueryContext.Provider value={{ query, setQuery }}>
<Header />
<Nav />
<Main />
<Footer />
</QueryContext.Provider>
);
};
export default App;
All components which require data from QueryContext could extract it via useContext(QueryContext). As bonus against props way here - you are allowed to place Header and Main components as deep as you want, they will still have access to context.
In my opinion, the best solution here is to implement 2 components for each working with query: 1-st one will work with props, the 2-nd one with context, but you probably dont need it as long as it could be overengineering. Here is the example:
interface Props {
query: string;
setQuery: (value: string) => void;
}
function Header(props: Props) {
return (...);
}
function HeaderConnected() {
const {query, setQuery} = useContext(QueryContext);
return <Header query={query} setQuery={setQuery}/>
}
This component separation method provides developer 2 flexible ways of using component. You could do the same with components, attached to Redux storage - writing their pure variant along with "connected" to storage.
My remarks for your code here:
In your case, there is no need to create several context providers placed on the same level with same value. Make it single and move to upper level.
In my opinion, there is no need to make such prop as value containing query and its setter. This is rather unclear, what value is and seems that these values should be extracted from some global scope (context), but it depends on task context.
I am trying to pull information from one component's API call to then use that data in another API call in a separate component. However, I am unsure how to export and use the data from the first API call in the second component.
App.js
import './App.css';
import FetchMatch from './fetch-match/fetch.match';
import FetchPlayer from './fetch-player/fetch.player';
function App() {
return (
<div className="App">
<h1>Hello world</h1>
<FetchPlayer></FetchPlayer>
<FetchMatch></FetchMatch>
</div>
);
}
export default App;
fetch.player then makes the first API call to get a users specific ID which will be used in the second API call too fetch that users match history.
fetch.player.js
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const FetchPlayer = () => {
const [playerData, setPlayerData] = useState([]);
const userName = 'users name';
const userTagLine = '1234';
const apiKey = '???';
useEffect( () => {
axios.get(`https://americas.api.riotgames.com/riot/account/v1/accounts/by-riot-id/${userName}/${userTagLine}?api_key=${apiKey}`)
.then(response => {
console.log(response.data)
setPlayerData([response.data])
})
.catch(error => console.log(error))
}, []);
return (
<div>
{playerData.map( data => (
<div>
<p>{data.puuid}</p>
<p>{data.gameName}#{data.tagLine}</p>
</div>
))}
</div>
)
}
export default FetchPlayer;
not much here but just in case...
fetch.match.js
import React, { useState } from 'react';
// Somehow take in the puuid set in the state of fetch.player to make a second API call below
const FetchMatch = () => {
const [matchData, setMatchData] = useState([]);
return (
<div>
// players match list goes here
</div>
)
}
export default FetchMatch;
I am unsure if I should make a separate function instead which would allow me to create consts to handle both API calls in a single file. Or if there is a way to pass the state from fetch.player as a prop to fetch.match from App.js. I have tried to do the former but it either doesn't work or I am messing up the syntax (most likely this)
If you render both component parallelly in a parent component, they are called sibling components.
Data sharing in sibling components can be done by multiple ways (Redux, Context etc) but the easiest and simplest way (the most basic way without 3rd party API) involves the use of parent as a middle component.
First you create the state in the parent component and provide it as props to the child component which need the data from its sibling (in your case is FetchMatch).
import React from 'react';
import './App.css';
import FetchMatch from './fetch-match/fetch.match';
import FetchPlayer from './fetch-player/fetch.player';
function App() {
const [data,setData] = React.useState();
return (
<div className="App">
<h1>Hello world</h1>
<FetchPlayer></FetchPlayer>
<FetchMatch data={data} ></FetchMatch>
</div>
);
}
export default App;
Provide the function to setData as a props to the child component which will fetch the initial API (in your case is FetchPlayer)
<FetchPlayer onPlayerLoad={(data) => setData(data)} />
Then, in that child component when you finish calling the API and get the result, pass that result to the onPlayerLoad function which will call the setData function with the result as parameters. It will lead to state change and re-rendering of the second FetchMatch component feeding the props data with API results.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const FetchPlayer = ({onPlayerLoad}) => {
const [playerData, setPlayerData] = useState([]);
const userName = 'users name';
const userTagLine = '1234';
const apiKey = '???';
useEffect( () => {
axios.get(`https://americas.api.riotgames.com/riot/account/v1/accounts/by-riot-id/${userName}/${userTagLine}?api_key=${apiKey}`)
.then(response => {
console.log(response.data)
setPlayerData([response.data])
onPlayerLoad(response.data)
})
.catch(error => console.log(error))
}, []);
return <></>;
Coming to FetchMatch, you will have the data in its second rendering.
import React, { useState } from 'react';
// Somehow take in the puuid set in the state of fetch.player to make a second API call below
const FetchMatch = ({data}) => {
const [matchData, setMatchData] = useState([]);
//console.log(data);
return (
<div>
// players match list goes here
</div>
)
}
export default FetchMatch;
Now, you can do whatever you want with the shared data in second component which in your case is trigger match API. 🎉
I'm trying to combine several contexts that are feed with some async operations, in my app's pages.
I would like to combine these contexts without using the Context.Provider because it could be verbose. For example,
<Route path="/discover">
<MainContainer extraClass="discover-container" hasHeader={true}>
<UserContext>
<ContentContextProvider>
<NotificationContext>
<Discover />
</NotificationContext>
</ContentContextProvider>
</UserContext>
</MainContainer>
</Route>
In each of these Context I wrapper the child with the context. Fe,
import React from "react";
import useAllContent from "utils/hooks/useAllContent";
const ContentContext = React.createContext({});
export const ContentContextProvider = ({ children }) => {
const { allContent, setAllContents } = useAllContent([]);
return (
<ContentContext.Provider value={{ allContent, setAllContents }}>
{children}
</ContentContext.Provider>
);
};
export default ContentContext;
This works, but as I mentioned before is very verbose so i would like to use the Contexts like an objetcs to combine between them.
I tried:
import { useState, useEffect, useCallback } from "react";
import { DataStore, Predicates } from "#aws-amplify/datastore";
import { Content } from "models";
const useAllContent = (initialValue) => {
const [allContent, setContent] = useState(initialValue);
const setAllContents = useCallback(async () => {
const contents = await DataStore.query(Content, Predicates.ALL);
setContent(contents);
}, []);
useEffect(() => {
if (allContent === 0) setAllContents();
}, [allContent, setAllContents]);
return { allContent, setAllContents };
};
export default useAllContent;
import React from "react";
import useAllContent from "utils/hooks/useAllContent";
const { allContent, setAllContents } = useAllContent([]);
const ContentContext = React.createContext({ allContent, setAllContents });
export default ContentContext;
But I break the rule × Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
How could i achieve it?
Combining instances of React.Context in the way you describe would require manipulating the values they contain. But there is no way to access the value of a Context without using its corresponding Provider. I sympathize with the dislike of verboseness, but it is unavoidable in this case.
I'm getting data via an Axios GET request from a local API and trying to save the data in a Context Object.
The GET request works properly when I run it outside the Context Provider function. But when I put it within a UseEffect function with no dependencies - ie. useEffect( () => /* do something*/, [] )the useEffect hook never fires.
Code here:
import React, { createContext, useReducer, useEffect } from 'react';
import rootReducer from "./reducers";
import axios from 'axios';
import { GET_ITEMS } from "./reducers/actions/types";
export const ItemsContext = createContext();
function ItemsContextProvider(props) {
const [items, dispatch] = useReducer(rootReducer, []);
console.log('this logs');
useEffect(() => {
console.log('this does not');
axios.get('http://localhost:27015/api/items')
.then(data => dispatch({type: GET_ITEMS, payload: data}))
}, [])
return (
<ItemsContext.Provider value={{items, dispatch}}>
{ props.children }
</ItemsContext.Provider>
);
}
export default ItemsContextProvider;
I never see 'this does not' in the console (double and triple checked). I'm trying to initialise the context to an empty value at first, make the GET request on first render, and then update the context value.
I'd really appreciate any help on what I'm doing wrong.
EDIT - Where Context Provider is being rendered
import React from 'react';
import AppNavbar from "./Components/AppNavbar";
import ShoppingList from "./Components/ShoppingList";
import ItemModal from "./Components/ItemModal";
//IMPORTED HERE (I've checked the import directory is correct)
import ItemsContextProvider from "./ItemsContext";
import { Container } from "reactstrap"
import "bootstrap/dist/css/bootstrap.min.css";
import './App.css';
function App() {
return (
<div className="App">
<ItemsContextProvider> //RENDERED HERE
<AppNavbar />
<Container>
<ItemModal />
<ShoppingList /> //CONSUMED HERE
</Container>
</ItemsContextProvider>
</div>
);
}
export default App;
I have it being consumed in another file that has the following snippet:
const {items, dispatch} = useContext(ItemsContext);
console.log(items, dispatch);
I see console logs showing the empty array I initialised outside the useEffect function in the Context Provider and also a reference to the dispatch function.
I had the same problem for quite a while and stumbled upon this thred which did not offer a solution. In my case the data coming from my context did not update after logging in.
I solved it by triggering a rerender after route change by passing in the url as a dependency of the effect. Note that this will always trigger your effect when moving to another page which might or might not be appropriate for your usecase.
In next.js we get access to the pathname by using useRouter. Depending on the framework you use you can adjust your solution. It would look something like this:
import React, { createContext, useReducer, useEffect } from 'react';
import rootReducer from "./reducers";
import axios from 'axios';
import { GET_ITEMS } from "./reducers/actions/types";
import { useRouter } from "next/router"; // Import the router
export const ItemsContext = createContext();
function ItemsContextProvider(props) {
const [items, dispatch] = useReducer(rootReducer, []);
const router = useRouter(); // using the router
console.log('this logs');
useEffect(() => {
console.log('this does not');
axios.get('http://localhost:27015/api/items')
.then(data => dispatch({type: GET_ITEMS, payload: data}))
}, [router.pathname]) // trigger useEffect on page change
return (
<ItemsContext.Provider value={{items, dispatch}}>
{ props.children }
</ItemsContext.Provider>
);
}
export default ItemsContextProvider;
I hope this helps anyone in the future!
<ItemsContextProvider /> is not being rendered.
Make sure is being consumed and rendered by another jsx parent element.
I am using react-router v4 for routing, to parse query params, it has been recommended to use history.listen here
I am calling it within the lifecycle hook componentDidMount to ensure that the component has mounted so that I can provide it as a piece of component state using a Higher Order Component like this:
import createHistory from 'history/createBrowserHistory';
import qs from 'qs';
import { compose, lifecycle, withState } from 'recompose';
export const history = createHistory();
const withQs = compose(
withState('search', 'setSearch', {}),
lifecycle({
componentDidMount: function () {
// works on the client side
history.listen(() => {
// history.location = Object.assign(history.location,
// parse the search string using your package of choice
// { query: parseQueryString(history.location.search) }
// )
console.log('History Location', history.location);
console.log('Search', history.location.search);
})
}
})
)
export default withQs
The history.listen is never triggered when a route navigates to the new page or a new query param is added to the page.
Per the React Router V4 docs, if you want to manage history yourself, you need to use <Router> instead of <BrowserRouter>:
import { Router } from 'react-router'
import createBrowserHistory from 'history/createBrowserHistory'
const history = createBrowserHistory()
<Router history={history}>
<App/>
</Router>