Would like to seek guidance from folks if this React implementation makes sense and understand the pitfalls if any. The implementation works but I am unsure if its the correct practice. Please kindly advise.
Idea - Create an AppContext that allows reusability of global states (or even functions) - instead of the conventional useContext + useReducer
AppContext.jsx
import React from 'react';
export const AppContext = React.createContext(null);
export const AppContextProvider = (props) => {
const [ appState, setAppState ] = React.useState({});
const appStateProvider = React.useMemo(() => ({ appState, setAppState }), [ appState, setAppState ]);
const setAppStateItem = (key, value) => {
appStateProvider.setAppState(state => { return { ...state, [key]: value} })
return value;
}
const getAppStateItem = (key = '', fallback) => {
return appState[key] || fallback;
}
const deleteAppStateItem = (key = '') => {
if(key in appState) {
appStateProvider.setAppState(state => {
state[key] = undefined;
return state;
})
}
}
return (
<AppContext.Provider value={{ appStateProvider, setAppStateItem, getAppStateItem, deleteAppStateItem }}>
{props.children}
</AppContext.Provider>
)
}
Create.jsx
import React from 'react';
import { AppContext } from 'contexts';
const { setAppStateItem } = React.useContext(AppContext);
....
setAppStateItem('count', 5);
....
Consume.jsx
import React from 'react';
import { AppContext } from 'contexts';
const { getAppStateItem, setAppStateItem } = React.useContext(AppContext);
....
const count = getAppStateItem('count');
....
Here was an approach to create a global state using useContext and useReducer following a pattern similar to redux. You essentially set up a Store with useReducer and a Context.Provider that you then wrap the rest of your application in. Here was a small implementation I had going:
import React, { createContext, useReducer } from "react";
import Reducer from './Reducer'
const initialState = {
openClose: false,
openOpen: false,
ticker: "BTCUSDT",
tickerRows: [],
positionRows: [],
prices: {},
walletRows: []
};
const Store = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>
{children}
</Context.Provider>
)
};
export const ACTIONS = {
SET_CLOSE_OPEN: 'SET_CLOSE_OPEN',
SET_OPEN_OPEN: 'SET_OPEN_OPEN',
SET_TICKER: 'SET_TICKER',
SET_TICKER_ROWS: 'SET_TICKER_ROWS',
SET_POSITION_ROWS: 'SET_POSITION_ROWS',
SET_PRICES: 'SET_PRICES',
SET_WALLET_ROWS: 'SET_WALLET_ROWS'
}
export const Context = createContext(initialState);
export default Store;
Here is the reducer:
import { ACTIONS } from './Store'
const Reducer = (state, action) => {
switch (action.type) {
case ACTIONS.SET_CLOSE_OPEN:
return {
...state,
openClose: action.payload
};
case ACTIONS.SET_OPEN_OPEN:
return {
...state,
openOpen: action.payload
};
...
default:
return state;
}
};
export default Reducer;
I put the Store component in index.js so that it's context is available to all of the components in the app.
ReactDOM.render(
<React.StrictMode>
<Store>
<App />
</Store>
</React.StrictMode>,
document.getElementById('root')
);
Then when you want to access and update the state, you just import the actions and context and the useContext hook:
import { useContext} from "react";
import { ACTIONS, Context } from './store/Store';
and then you just add
const [state, dispatch] = useContext(Context);
inside your functional component and you can access the state and use dispatch to update it.
One limitation is that every component that accesses the state with useContext re-renders every time anything in the state gets updated, not just the part of the state that the component depends on. One way around this is to use the useMemo hook to control when the component re-renders.
export default function WalletTable({ priceDecimals }) {
const classes = useStyles();
const [state, dispatch] = useContext(Context);
async function getCurrentRows() {
const response = await fetch("http://localhost:8000/wallet");
const data = await response.json();
dispatch({ type: ACTIONS.SET_WALLET_ROWS, payload: data });
}
useEffect(() => {
getCurrentRows();
}, []);
const MemoizedWalletTable = useMemo(() => {
return (
<TableContainer component={Paper}>
...
</TableContainer>
);
}, [state.walletRows])
return MemoizedWalletTable
}
Having to memoize everything makes it seem like maybe just using redux isn't all that much more complicated and is easier to deal with once set up.
Related
I am new to this so please bear with me, I am trying to pass the products & the empty cart from (Context.js) to the (cartState) in apps.js file, through useReducer as intructed in minute 20 of this guide, the only difference is that he is using a faker to generate the Api and I am pulling data with Axios in a useEffect from Api with 13 products
I tried in many ways to receive the items inside the products array but always receiving an empty array in console log of the (carState) in App.js File, all help is welcome appreciated ! Github Repo
Context.js
import { createContext, useState, useEffect, useReducer, useContext } from 'react';
import { cartReducer } from './Reducers';
import { ApiServer } from '../services/Api/index'
const Cart = createContext();
const Context = ({ children }) => {
const [products, setProducts] = useState([]);
useEffect(() => {
ApiServer.get('/products/')
.then(response => {
if (response.data != null) {
setProducts(response.data);
} else {
console.log('O Array esta vazio')
}
})
.catch(error => {
console.log(error);
});
}, []);
console.log(products)
const [state, dispatch] = useReducer(cartReducer, {
products: products,
cart: [],
});
return <Cart.Provider value={{ state, dispatch }}> {children}</Cart.Provider>;
}
export default Context
export const CartState = () => {
return useContext(Cart);
};
Reducer.js
export const cartReducer = (state, action) => {
switch (action.type) {
default:
return state;
}
};
App.js
import React from 'react';
import Router from './routes';
import GlobalStyle from './assets/styles/globalStyles';
import ScrollToTop from './components/ScrollToTop';
import { BrowserRouter } from 'react-router-dom';
import { ContentProvider } from './useContext';
import { CartState } from './contexts/Context';
function App() {
const { state } = CartState();
console.log(state);
return (
<BrowserRouter>
<ContentProvider>
<GlobalStyle />
<Router />
<ScrollToTop />
</ContentProvider>
</BrowserRouter>
);
}
export default App;
What you are trying to do here will not work. The initial state of the reducer is just that -- the INITIAL state. It will use the value of initialState from the very first render, before your items has been updated with the fetch results. Changes to the variable initialState will not effect the reducer after it has already been created. You have to dispatch an action to store the items in your reducer. source
Solution:
As you are using useReducer() for state handling you don't need to use useState()
Write your useReducer like:
const [state, dispatch] = useReducer(reducer, {
products: [],
cart: []
});
Reducer function will be like:
export const reducer = (state, action) => {
switch (action.type) {
case "SET_PRODUCTS_DATA":
return { ...state, products: action.payload };
default:
return state;
}
}
Set product data from useEffect using dispatch like:
useEffect(() => {
ApiServer.get("/products/")
.then((response) => {
if (response.data != null) {
dispatch({ type: "SET_PRODUCTS_DATA", payload: response.data });
} else {
console.log("O Array esta vazio");
}
})
.catch((error) => {
console.log(error);
});
}, []);
It should work perfectly. Demo codesandbox Link
So I've been learning to use useContext and useReducer hooks with action/dispatch and whenever I try to use dipatch function from any component, it throws out "Uncaught TypeError: dispatch is not a function" error.. it's been 5 days now:/
I try to access dispatch function in the following manner inside components
// context
import { ModalContext } from "../Context/Contexts/ModalContext";
import { OPEN_IMAGE_MODAL } from "../Context/action.types";
const { dispatch } = useContext(ModalContext);
dispatch({
type: OPEN_IMAGE_MODAL,
payload: { isEnabled: true, imageDetails: { url: doc.url } },
});
Here are the ref files
App.js
import React from "react";
// components
import Nav from "./Components/Nav";
import ImageUploadForm from "./Components/ImageUploadForm";
import ImageGrid from "./Components/ImageGrid";
// context
import { ModalContextProvider } from "./Context/Contexts/ModalContext";
const App = () => {
return (
<div className="App">
<Nav />
<ImageUploadForm />
<ModalContextProvider>
<ImageGrid/>
</ModalContextProvider>
</div>
);
};
export default App;
ModalContext.js (Context & Context Provider creation)
import { createContext, useReducer } from "react";
// reducer
import { modalReducer } from "../Reducers/modalReducer";
// components
import ImageModal from "../../Components/ImageModal";
// creating and exporting context
export const ModalContext = createContext();
const initialState = { isEnabled: false, imageDetails: {} };
export const ModalContextProvider = ({ children }) => {
// for selected/clicked image
const [isEnabled, imageDetails, dispatch] = useReducer(
modalReducer,
initialState
);
return (
<ModalContext.Provider value={{ isEnabled, imageDetails, dispatch }}>
{children}
{isEnabled && <ImageModal />}
</ModalContext.Provider>
);
};
modalReducer.js (reducer fn)
import { OPEN_IMAGE_MODAL, CLOSE_IMAGE_MODAL } from "../action.types";
export const modalReducer = (state, action) => {
switch (action.type) {
case OPEN_IMAGE_MODAL:
return {
...state,
isEnabled: true,
imageDetails: action.payload.imageDetails,
};
case CLOSE_IMAGE_MODAL:
return { ...state, isEnabled: false, imageDetails: {} };
default:
return { ...state };
}
};
useReducer returns an array with exactly two values. You are assuming there is some third value dispatch, which is actually undefined. And undefined is not a function.
Depending on how many values you want to passdown using context you can fix this. I like to pass only two values, one state and one dispatch.
const [state, dispatch] = useReducer(
modalReducer,
initialState
);
return (
<ModalContext.Provider value={{ state, dispatch }}>
{children}
{state.isEnabled && <ImageModal />}
</ModalContext.Provider>
);
In children just extract how you do:
const { dispatch } = useContext(ModalContext);
I think the useReducer hook just can return 2 arrays, the satate and the dsipatch method.
I have set a basic sample project that use Context to store the page title, but when I set it the component is not rerendered.
Principal files:
Context.js
import React from 'react'
const Context = React.createContext({})
export default Context
AppWrapper.js
import React from 'react'
import App from './App'
import Context from './Context'
function AppWrapper () {
return (
<Context.Provider value={{page: {}}}>
<App />
</Context.Provider>
)
}
export default AppWrapper
App.js
import React, { useContext } from 'react';
import Context from './Context';
import Home from './Home';
function App() {
const { page } = useContext(Context)
return (
<>
<h1>Title: {page.title}</h1>
<Home />
</>
);
}
export default App;
Home.js
import React, { useContext } from 'react'
import Context from './Context'
function Home () {
const { page } = useContext(Context)
page.title = 'Home'
return (
<p>Hello, World!</p>
)
}
export default Home
full code
What am I doing wrong?
Think about React context just like you would a component, if you want to update a value and show it then you need to use state. In this case your AppWrapper where you render the context provider is where you need to track state.
import React, {useContext, useState, useCallback, useEffect} from 'react'
const PageContext = React.createContext({})
function Home() {
const {setPageContext, page} = useContext(PageContext)
// essentially a componentDidMount
useEffect(() => {
if (page.title !== 'Home')
setPageContext({title: 'Home'})
}, [setPageContext])
return <p>Hello, World!</p>
}
function App() {
const {page} = useContext(PageContext)
return (
<>
<h1>Title: {page.title}</h1>
<Home />
</>
)
}
function AppWrapper() {
const [state, setState] = useState({page: {}})
const setPageContext = useCallback(
newState => {
setState({page: {...state.page, ...newState}})
},
[state, setState],
)
const getContextValue = useCallback(
() => ({setPageContext, ...state}),
[state, updateState],
)
return (
<PageContext.Provider value={getContextValue()}>
<App />
</PageContext.Provider>
)
}
Edit - Updated working solution from linked repository
I renamed a few things to be a bit more specific, I wouldn't recommend passing setState through the context as that can be confusing and conflicting with a local state in a component. Also i'm omitting chunks of code that aren't necessary to the answer, just the parts I changed
src/AppContext.js
export const updatePageContext = (values = {}) => ({ page: values })
export const updateProductsContext = (values = {}) => ({ products: values })
export const Pages = {
help: 'Help',
home: 'Home',
productsList: 'Products list',
shoppingCart: 'Cart',
}
const AppContext = React.createContext({})
export default AppContext
src/AppWrapper.js
const getDefaultState = () => {
// TODO rehydrate from persistent storage (localStorage.getItem(myLastSavedStateKey)) ?
return {
page: { title: 'Home' },
products: {},
}
}
function AppWrapper() {
const [state, setState] = useState(getDefaultState())
// here we only re-create setContext when its dependencies change ([state, setState])
const setContext = useCallback(
updates => {
setState({ ...state, ...updates })
},
[state, setState],
)
// here context value is just returning an object, but only re-creating the object when its dependencies change ([state, setContext])
const getContextValue = useCallback(
() => ({
...state,
setContext,
}),
[state, setContext],
)
return (
<Context.Provider value={getContextValue()}>
...
src/App.js
...
import AppContext, { updateProductsContext } from './AppContext'
function App() {
const [openDrawer, setOpenDrawer] = useState(false)
const classes = useStyles()
const {
page: { title },
setContext,
} = useContext(Context)
useEffect(() => {
fetch(...)
.then(...)
.then(items => {
setContext(updateProductsContext({ items }))
})
}, [])
src/components/DocumentMeta.js
this is a new component that you can use to update your page names in a declarative style reducing the code complexity/redundancy in each view
import React, { useContext, useEffect } from 'react'
import Context, { updatePageContext } from '../Context'
export default function DocumentMeta({ title }) {
const { page, setContext } = useContext(Context)
useEffect(() => {
if (page.title !== title) {
// TODO use this todo as a marker to also update the actual document title so the browser tab name changes to reflect the current view
setContext(updatePageContext({ title }))
}
}, [title, page, setContext])
return null
}
aka usage would be something like <DocumentMeta title="Whatever Title I Want Here" />
src/pages/Home.js
each view now just needs to import DocumentMeta and the Pages "enum" to update the title, instead of pulling the context in and manually doing it each time.
import { Pages } from '../Context'
import DocumentMeta from '../components/DocumentMeta'
function Home() {
return (
<>
<DocumentMeta title={Pages.home} />
<h1>WIP</h1>
</>
)
}
Note: The other pages need to replicate what the home page is doing
Remember this isn't how I would do this in a production environment, I'd write up a more generic helper to write data to your cache that can do more things in terms of performance, deep merging.. etc. But this should be a good starting point.
Here is a working version of what you need.
import React, { useState, useContext, useEffect } from "react";
import "./styles.css";
const Context = React.createContext({});
export default function AppWrapper() {
// creating a local state
const [state, setState] = useState({ page: {} });
return (
<Context.Provider value={{ state, setState }}> {/* passing state to in provider */}
<App />
</Context.Provider>
);
}
function App() {
// getting the state from Context
const { state } = useContext(Context);
return (
<>
<h1>Title: {state.page.title}</h1>
<Home />
</>
);
}
function Home() {
// getting setter function from Context
const { setState } = useContext(Context);
useEffect(() => {
setState({ page: { title: "Home" } });
}, [setState]);
return <p>Hello, World!</p>;
}
Read more on Hooks API Reference.
You may put useContext(yourContext) at wrong place.
The right position is inner the <Context.Provider>:
// Right: context value will update
<Context.Provider>
<yourComponentNeedContext />
</Context.Provider>
// Bad: context value will NOT update
<yourComponentNeedContext />
<Context.Provider>
</Context.Provider>
Need help, just started to learn React. I'm trying to pass variables with json data to a component for further use, but catching the errors. what should I change to use variables with json() data from Store.js in the product.js component? THanks for your time!
https://jsfiddle.net/constant101/xu7zdn26/3/ for better visibility
//Store export(receiving data from the server and assigning them to variables)
import React, {useState, useEffect} from 'react'
import axios from 'axios'
export const ListContext = React.createContext([]);
export const ItemContext = React.createContext([]);
function Store() {
const [storeProducts, setStoreProducts] = useState([]);
const [detailProduct, setDetailProduct] = useState([]);
useEffect(() => {
axios.get('/products/')
.then(res => {
console.log(res)
setStoreProducts(res.data)
})
},[])
console.log('storeProducts:', storeProducts)
useEffect(() => {
axios.get('/products/:productId')
.then(res => {
console.log(res)
setDetailProduct(res.data)
})
},[])
console.log('detail product:', detailProduct)
return (
<ListContext.Provider value={[storeProducts, setStoreProducts]}>
<ItemContext.Provider value={[detailProduct, setDetailProduct]}>
<product/>
</ItemContext.Provider>
</ListContext.Provider>
);
}
export const detailProduct
//product.js ( file that uses data from the fetch)
import React, { useReducer, createContext, useContext, useState } from 'react';
import {ListContext, ItemContext } from '../Store';
import { useProductActions } from '../actions';
import { SET_PRODUCT_DETAILS } from '../actions/types';
const [storeProducts] = useContext(ListContext);
const [detailProduct] = useContext(ItemContext);
let tempProducts = [];
storeProducts.forEach(item => tempProducts.push({ ...item })
);
const initialState = {
products: tempProducts,
productDetails: { ...detailProduct }
};
console.log(storeProducts)
const productReducer = (state, action) => {
switch (action.type) {
case SET_PRODUCT_DETAILS:
return {
...state,
productDetails: action.payload
};
default:
throw new Error('Invalid action type');
}
};
export const ProductContext = createContext(initialState);
export const useProductState = () => {
return useContext(ProductContext);
};
export const ProductProvider = ({ children }) => {
const [state, dispatch] = useReducer(productReducer, initialState);
const productActions = useProductActions(state, dispatch);
return (
<ProductContext.Provider value={{ productState: state, productActions }}>
{children}
</ProductContext.Provider>
);
};
Well, assuming your request is right, i saw a syntax mistake. You should pass
<ListContext.Provider value={{storeProducts, setStoreProducts}}> instead of
<ListContext.Provider value={[storeProducts, setStoreProducts]}>
The reason:
a provider requires a prop called value with an Object inside.
In that case, you were passing an array.
it would be the same if you did:
<ListContext.Provider
value={{
storeProducts: storeProducts,
setStoreProducts: setStoreProducts
}}
>
but to follow the DRY principle, it's recommended to do that way described earlier
I am trying to use React context as a state manager in my React Native app.
Here's the context:
import React, { createContext, useState, useEffect } from "react";
import axios from "axios";
export const GlobalContext = createContext();
export const Provider = ({ children }) => {
const [tracksList, setTracksList] = useState([
{
track_list: []
}
]);
useEffect(() => {
axios
.get(
`https://cors-anywhere.herokuapp.com/https://api.musixmatch.com/ws/1.1/chart.tracks.get?page=1&page_size=10&country=us&f_has_lyrics=1&apikey=${
process.env.REACT_APP_MM_KEY
}`
)
.then(res => {
setTracksList([
{
track_list: res.data.message.body.track_list
}
]);
})
.catch(err => console.log(err));
}, []);
return (
<GlobalContext.Provider value={[tracksList, setTracksList]}>
{children}
</GlobalContext.Provider>
);
};
export const Consumer = GlobalContext.Consumer;
Child component. Here I'd like to make an API call to get users and set this users field to global context. I can get context value from consumer, but how to set the new one?
import React, { useContext } from "react";
import { GlobalContext } from "../../context/context";
const Demo = () => {
const contextValue = useContext(GlobalContext);
console.log(contextValue, "Context outside from JSX");
return <div>Content</div>;
};
export default Demo;
So, is it possible to add new value to React context from every child component, like in Redux? Thanks in advance!
You could use the useReducer effect to achieve Redux reducers:
// Create context
export const ApiContext = React.createContext();
// Create some reducer function
const reducer = (state, action) => {
if (action.type === 'some action name') {
return {
...state,
report: action.payload,
};
}
return state;
};
// Overwrite a context provider
const Provider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, {});
return (
<ApiContext.Provider
value={{
...state,
dispatch,
}}
>
{children}
</ApiContext.Provider>
);
};
Then you could use in components as follows:
const Component = () => {
const { dispatch, report } = React.useContext(ApiContext);
React.useEffect(() => {
const asyncPost = async () => {
const response = await fetch('some endpoint', {
method: 'POST',
});
const payload = await response.json();
// This will call a reducer action and update a state
dispatch({
type: 'some action name',
payload,
});
}
}, []);
...
};
So when Component is mounted, the state would be an empty object. Then when you update the state using the some action name action, the state becomes { report: some data from fetch }.