Just stumbled upon the question of writing custom redux hooks.
Here is a backbone of the App.js
import { Provider, useStore, useDispatch } from "./redux";
import reducers from "./reducers";
import "./styles.css";
import React} from "react";
function Hello() {
const counter = useStore(store => store.counter);
const dispatch = useDispatch();
const increment = () => dispatch({ type: "INCREMENT" });
const decrement = () => dispatch({ type: "DECREMENT" });
return (
<div>
<h3>Counter: 0</h3>
<button onClick={increment}>+ Increment</button>
<button onClick={decrement}>- Decrement</button>
</div>
);
}
// <Provider reducers={reducers}>
export default function App() {
return (
<div className="App">
<Provider reducers={reducers}>
<Hello />
</Provider>
</div>
);
}
The idea is to write implementations for useStore, useDispatch and Provider.
I got the idea that we should use context api and useReducer to have access to dispatch but then I got stuck.
I know Provider can something be like
const initialState = {};
const context = createContext(initialState);
export function Provider({ children, reducers }) {
const [state, dispatch] = useReducer(reducers, initialState);
// children need to have access to dispatch
return (
<context.Provider value={{ state, dispatch }}>{children}</context.Provider>
);
}
And similarly we could have used useContext to have access to the value passed
like
export function useStore(selector) {
const { state} = useContext(context);
return { state.selector};
}
/**
* Returns the dispatch function
*/
export function useDispatch() {
const { dispatch } = useContext(context);
return { dispatch };
}
reducers file is also a part of the sandbox link given below.
But I am stuck and cannot figure why it's not working.
Please help and explain.
Here is the work in progress sandbox
https://codesandbox.io/s/redux-forked-tlb4hb?file=/src/redux.js
Related
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.
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.
Currently, I'm using functional components with hooks but still dispatching my actions with the connect HOC.
I read through the documentation with useDispatch but I'm unsure how to incorporate it in my code. From the examples, they are passing the the action types and payloads inside the component. Would I have to move myOfferActions functions back to the component in order to useDispatch?
MyOffers component
import React, { useEffect } from "react";
import { connect, useSelector } from "react-redux";
import "./MyOffers.scss";
import MyOfferCard from "../../components/MyOfferCard/MyOfferCard";
import { fetchMyOffers } from "../../store/actions/myOffersActions";
const MyOffers = (props) => {
const myOffers = useSelector((state) => state.myOffers.myOffers);
useEffect(() => {
props.fetchMyOffers();
}, []);
return (
<div className="my-offers-main">
<h1>My offers</h1>
{myOffers && (
<div className="my-offer-container">
{myOffers.map((offer) => (
<MyOfferCard key={offer.id} offer={offer} />
))}
</div>
)}
</div>
);
};
export default connect(null, { fetchMyOffers })(MyOffers);
offerActions
export const fetchMyOffers = () => async (dispatch) => {
const userId = localStorage.getItem("userId");
try {
const result = await axiosWithAuth().get(`/offers/${userId}`);
let updatedData = result.data.map((offer) => {
//doing some stuff
};
});
dispatch(updateAction(FETCH_MY_OFFERS, updatedData));
} catch (error) {
console.log(error);
}
};
offerReducer
import * as types from "../actions/myOffersActions";
const initialState = {
offerForm: {},
myOffers: [],
};
function myOffersReducer(state = initialState, action) {
switch (action.type) {
case types.FETCH_MY_OFFERS:
return {
...state,
myOffers: action.payload,
};
default:
return state;
}
}
export default myOffersReducer;
I don't think you need connect when using the redux hooks.
You just need to call useDispatch like:
const dispatch = useDispatch();
And use the function by providing the object identifying the action:
dispatch({ type: 'SOME_ACTION', payload: 'my payload'});
It should be working with redux-thunk too (I guess this is what you're using): dispatch(fetchMyOffers())
reducer.js
export const reducer = (state, action) => {
switch(action.type) {
case SET_HEROES:
return { ...state, heroes: action.heroes }
}
}
AppContext.js
export const AppContext = React.createContext()
export const AppProvider = (props) => {
const initialState = {
heroes: []
}
const [appState, dispatch] = useReducer(reducer, initialState)
const setHeroes = async () => {
const result = await getHeroes()
dispatch({ type: SET_HEROES, heroes: result })
}
return <AppContext.Provider
values={{ heroes: appState.heroes, setHeroes }}
>
{props.children}
</AppContext.Provider>
}
HeroesScreen.js
const HeroesScreen = () => {
const { heroes, setHeroes } = useContext(AppContext)
useEffect(() => {
setHeroes()
}, [])
return <>
// iterate hero list
</>
}
export default HeroesScreen
Above is plain simple setup of a component using reducer + context as state management. Heroes are showing on the screen, everything works fine but I'm having a warning Reach Hook useEffect has a missing dependency: 'setHeroes'. But if I add it in as a dependency, it'll crash my app with Maximum depth update exceeded
Been searching but all I see is putting the function fetch call inside the useEffect(). What I would like is to extract the function and put it in a separate file following the SRP principle
EDITED:
As advised on using useCallback()
AppContext.js
const setHeroes = useCallback(() => {
getHeroes().then(result => dispatch({ type: SET_HEROES, heroes: result }))
}, [dispatch, getHeroes])
HeroesScreen.js
useEffect(() => {
setHeroes()
}, [setHeroes])
Adding the getHeroes as dependency on useCallback, linter shows unnecessary dependency
If you look at the code in AppProvider, you are creating a new setHeroes function every time it renders. So if you add setHeroes as a dependency to useEffect the code executes something like this:
AppProvider renders, setHeroes is created, the state is initial state
Somewhere down the component hierarchy HeroesScreen renders. useEffect is called which in turn calls setHeroes
getHeroes is called and an action is dispatched
reducer changes the state which causes AppProvider to re-render
AppProvider renders, setHeroes is created from scratch
The useEffect executes again since setHeroes changed and the whole loop repeats forever!
To fix the issue you indeed need to add setHeroes as a dependency to useEffect but then wrap it using useCallback:
const setHeroes = useCallback(async () => {
const result = await getHeroes();
dispatch({ type: SET_HEROES, heroes: result });
}, [getHeroes, dispatch]);
Good question, I also had this problem, this is my solution. I'm using Typescript but it'll work with JS only as well.
UserProvider.tsx
import * as React from 'react'
import { useReducer, useEffect } from 'react'
import UserContext from './UserContext'
import { getUser } from './actions/profile'
import userReducer, { SET_USER } from './UserReducer'
export default ({ children }: { children?: React.ReactNode }) => {
const [state, dispatch] = useReducer(userReducer, {})
const getUserData = async () => {
const { user } = await getUser()
dispatch({ type: SET_USER, payload: { ...user } })
}
useEffect(() => {
getUserData()
}, [])
return (
<UserContext.Provider value={{ user: state }}>
{children}
</UserContext.Provider>
)
}
Then wrap your App with the provider
index.tsx
<UserProvider>
<App />
</UserProvider>
Then to use the Context Consumer I do this
AnyComponent.tsx
<UserConsumer>
{({ user }) => {
...
}}
</UserConsumer
or you can also use it like this
const { user } = useContext(UserContext)
Let me know if it works.
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>