How to dispatch action in Custom Hooks by useReducer and useContext? - javascript

I created a sample for a button toggle.
This is done by useContext (store the data) and useReducer (process the data). and it is working fine.
Here's the CodeSandBox Link to how it works.
version 1 is just dispatch when clicking the button.
Then I created a version 2 of toggling. basically just put the dispatch inside a custom hook. but somehow, it doesn't work.
// context
export const initialState = { status: false }
export const AppContext = createContext({
state: initialState,
dispatch: React.dispatch
})
// reducer
const reducer = (state, action) => {
switch (action.type) {
case 'TOGGLE':
return {
...state,
status: action.payload
}
default:
return state
}
}
//custom hook
const useDispatch = () => {
const {state, dispatch} = useContext(AppContext)
return {
toggle: dispatch({type: 'UPDATE', payload: !state.status})
// I tried to do toggle: () => dispatch(...) as well
}
}
// component to display and interact
const Panel = () => {
const {state, dispatch} = useContext(AppContext)
// use custom hook
const { toggle } = useDispatch()
const handleChange1 = () => dispatch({type: 'TOGGLE', payload: !state.status})
const handleChange2 = toggle // ERROR!!!
// and I tried handleChange2 = () => toggle, or, handleChange2 = () => toggle(), or handleChange2 = toggle()
return (
<div>
<p>{ state.status ? 'On' : 'Off' }</p>
<button onClick={handleChange1}>change version 1</button>
<button onClick={handleChange2}>change version 2</button>
</div>
)
}
// root
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<AppContext.Provider value={{state, dispatch}}>
<div className="App">
<Panel />
</div>
</AppContext.Provider>
);
}
Not sure what's going there. but I think there's something wrong with the dispatched state.
(I tried it works if the payload is not processing state, like some hard code stuff, so the dispatch should be fired at this moment)
Could someone give me a hand? Appreciate!!!

You are correct that toggle needs to be a function but you are dispatching action type UPDATE and the reducer doesn't do anything with that action.
Dennis is correct that there is no point in the initial value you are giving the context and may as well leave it empty as the provider will provide the value.
The useMemo suggestion from Dennis will not optimize your example since App re renders when state changes so the memoized value will never be used.
Here is a working example of your code with comments what I changed:
const { createContext, useReducer, useContext } = React;
const initialState = { status: false };
//no point in setting initial context value
const AppContext = createContext();
const reducer = (state, action) => {
switch (action.type) {
case 'TOGGLE':
return {
...state,
status: action.payload,
};
default:
return state;
}
};
const useDispatch = () => {
const { state, dispatch } = useContext(AppContext);
return {
//you were correct here, toggle
// has to be a function
toggle: () =>
dispatch({
//you dispatch UPDATE but reducer
// is not doing anything with that
type: 'TOGGLE',
payload: !state.status,
}),
};
};
const Panel = () => {
const { state, dispatch } = useContext(AppContext);
const { toggle } = useDispatch();
const handleChange1 = () =>
dispatch({ type: 'TOGGLE', payload: !state.status });
const handleChange2 = toggle; // ERROR!!!
return (
<div>
<p>{state.status ? 'On' : 'Off'}</p>
<button onClick={handleChange1}>
change version 1
</button>
<button onClick={handleChange2}>
change version 2
</button>
</div>
);
};
function App() {
const [state, dispatch] = useReducer(
reducer,
initialState
);
return (
<AppContext.Provider value={{ state, dispatch }}>
<div className="App">
<Panel />
</div>
</AppContext.Provider>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Well, there no such thing React.dispatch. Its value is undefined
export const AppContext = createContext({
state: initialState,
// useless
// dispatch: undefined
dispatch: React.dispatch
});
// dispatch function won't trigger anything.
const {state, dispatch} = useContext(AppContext);
version 1 is actually how context should be used, although usually, you will want to add an extra memoization step (depending on the use case), because on every render you assign a new object {state,dispatch} which always will cause a render even though state may be the same.
See such memoization use case example.
If my point wasn't clear, see HMR comment:
Strategic useMemo should be used, if many components access the
context then memoizing is a good idea when the component with the
provider re-renders for reasons other than changing the context.

Related

Why my react app rendered twice and give me an empty array in first render when I'm trying fetching API?

The second render was success but the side effect is my child component's that using context value from its parent as an initialState (using useState hooks) set its initial state to empty array. I'm using React Hooks ( useState, useContext, useReducer, useEffect)
App.js:
...
export const MainContext = React.createContext();
const initialState = {
data: [],
};
const reducer = (state, action) => {
switch (action.type) {
case "ADD_LIST":
return { ...state, data: action.payload };
default:
return state;
}
};
function App() {
const [dataState, dispatch] = useReducer(reducer, initialState);
const fetchData = () => {
axios
.get("http://localhost:3005/data")
.then((response) => {
const allData = response.data;
if (allData !== null) {
dispatch({ type: "ADD_LIST", payload: allData });
}
})
.catch((error) => console.error(`Error: ${error}`));
};
useEffect(() => {
fetchData();
}, []);
console.log("useReducer", dataState); //LOG 1
return (
<div>
<MainContext.Provider
value={{ dataState: dataState, dataDispatch: dispatch }}
>
<MainPage />
</MainContext.Provider>
</div>
);
}
export default App;
My child component
MainPage.jsx
...
function MainPage() {
const mainContext = useContext(MainContext);
const data = mainContext.dataState.data;
const uniqueList = [...new Set(data.map((list) => list.type))];
console.log("uniqueList", uniqueList); // LOG 2
const [uniqueType, setUniqueType] = useState(uniqueList);
console.log("uniqueType State", uniqueType); // LOG 3
const catAlias = {
account: "Account",
commandLine: "Command",
note: "Note",
bookmark: "Bookmark",
};
return (
// jsx code, not necessary with the issue
)
};
The result :
result image
As you can see, uniqueType state is still empty eventhough the uniqueList is already with filled array. My goal is to make uniqueType initial state into the uniqueList from the first render.
That's the expected behavior. The value used for state initialization is only used in first render. In subsequent renders, the component needs to use useEffect to keep track of value changes.
UseEffect(()=>{
// unique list update.
}, [data]);

React useReducer don't update state in React context

React useReducer don't update state in React context. But in return section state data render correctly. Here is sample:
context.js
const globalContext = React.createContext();
const initialState = {
statuses: null,
};
const globalReducer = (state, action) => {
switch (action.type) {
case 'SET_STATUSES':
return { ...state, statuses: action.payload };
default:
return state;
}
};
export const GlobalState = ({ children }) => {
const [state, dispatch] = React.useReducer(globalReducer, initialState);
return <globalContext.Provider value={{ state, dispatch }}>{children}</globalContext.Provider>;
};
export const useGlobalState = () => {
const context = React.useContext(globalContext);
return context;
};
comeChild.js
const { state, dispatch } = useGlobalState();
const testFn = () => {
console.log(state); // -> {statuses: null} :here is issue
};
React.useEffect(() => {
console.log(state); // -> {statuses: null} :as expected
dispatch({ type: 'SET_STATUSES', payload: 'test str' });
console.log(state); // -> {statuses: null} :here is issue
testFn();
setTimeout(() => {
console.log(state); // -> {statuses: null} :here is issue
}, 3000);
}, []);
return <div>
{state.statuses && <div>{state.statuses}</div>}// -> 'test str'
</div>;
What could be the issue?
Im fairly new to contexts and the useReducer hook my self, but my guess is the good ol' state "logic" in React. React update states asynchronous and not synchronous which can result in these behaviours.
Your reducer and context obviously works, since it outputs the correct state in your return statement. This is because of your state.statuses && condition, indicating that you want to return the div WHEN state.statuses "exist" so to say.
So to me it doesn't look like any problem, just that React being React with the state updates.
You could console.log(action.payload) in your reducer to see when 'test str' enters the reducer action.

How to reference a react component's state in a redux helper function

I'm new to redux, and I can find lots of info on how to pass a redux state to the component, but not the other way round, so I'm not sure if I'm searching the correct vocabulary. But Essentially I want to be able to reference the current state of a react component in a redux helper function, this is what I've done - and I'm getting TypeError: dispatch is not a function and handleSubmit is just launching as soon as the page is loaded:
App.js
render() {
return (
<div className="App">
<p>{this.state.id}</p>
<form onSubmit={this.handleSubmit(this.state.id)}>
<button type="submit">Submit</button>
</form>
</div>
)
}
const mapDispatchToProps = dispath => bindActionCreators({
handleSubmit
}, dispath);
export default connect(
mapDispatchToProps
)(App);
reducers.js
export const handleSubmit = (test) => {
window.open("http://localhost:5000/"+test);
}
//Reducer
export default (state = [], action) => {
switch (action.type) {
default:
return state;
}
};
First, you don't use the function that react-redux pass through the props and try to call handleSubmit on the component itself.
You are also calling the function inside onSubmit immediately instead of passing a reference to a function so wrap it in an arrow function and use handleSubmit from this.props
onSubmit={() => this.props.handleSubmit(this.state.id)}
Second, the first argument to connect is the mapping function to get a slice of the state called mapStateTpProps by convention, pass in null as the first argument.
there is also no need to use bindActionCreators and you can just pass an object with functions and react-redux will wrap them in dispatch for you
export default connect(
null,
{ handleSubmit }
)(App);
You need to put id to the state of App and manage it through redux.
Code below will help you.
// App.js
render() {
return (
<div className="App">
<p>{this.props.id}</p>
<form onSubmit={this.props.ActionSubmit(this.props.id)}>
<button type="submit">Submit</button>
</form>
</div>
)
}
const mapStateToProps = store => ({
id: store.appReducer.id,
})
const mapDispatchToProps = dispath => bindActionCreators({
ActionSubmit
}, dispath);
export default connect(
mapStateToProp,
mapDispatchToProps
)(App);
// reducers.js
export ACTION_SUBMIT = 'ACTION_SUBMIT'
export const ActionSubmit = id => ({
type: ACTION_SUBMIT,
payload: {
id,
}
})
const initialState = {
id: 0,
}
const doSubmit = (id) => {
window.open("http://localhost:5000/"+id);
}
export default AppReducer(state = initialState, action) {
switch(action.type) {
case ACTION_SUBMIT:
doSubmit( action.payload.id)
return {
id: action.payload.id,
}
default:
return state
}
}

Unable to read state updated by useReducer hook in context provider

I am using useReducer hook to manage my state, but it seems like I have a problem with reading updated state in my context provider.
My context provider is responsible to fetch some remote data and update the state based on responses:
import React, { useEffect } from 'react';
import useAppState from './useAppState';
export const AppContext = React.createContext();
const AppContextProvider = props => {
const [state, dispatch] = useAppState();
const initialFunction = () => {
fetch('/some_path')
.then(res => {
dispatch({ type: 'UPDATE_STATE', res });
});
};
const otherFunction = () => {
fetch('/other_path')
.then(res => {
// why is `state.stateUpdated` here still 'false'????
dispatch({ type: 'DO_SOMETHING_ELSE', res });
});
}
};
const actions = { initialFunction, otherFunction };
useEffect(() => {
initialFunction();
setInterval(otherFunction, 30000);
}, []);
return (
<AppContext.Provider value={{ state, actions }}>
{props.children}
</AppContext.Provider>
)
};
export default AppContextProvider;
and useAppState.js is very simple as:
import { useReducer } from 'react';
const useAppState = () => {
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_STATE':
return {
...state,
stateUpdated: true,
};
case 'DO_SOMETHING_ELSE':
return {
...state,
// whatever else
};
default:
throw new Error();
}
};
const initialState = { stateUpdated: false };
return useReducer(reducer, initialState);
};
export default useAppState;
The question is, as stated in the comment above, why is state.stateUpdated in context provider's otherFunction still false and how could I access state with latest changes in the same function?
state will never change in that function
The reason state will never change in that function is that state is only updated on re-render. Therefore, if you want to access state you have two options:
useRef to see a future value of state (you'll have to modify your reducer to make this work)
const updatedState = useRef(initialState);
const reducer = (state, action) => {
let result;
// Do your switch but don't return, just modify result
updatedState.current = result;
return result;
};
return [...useReducer(reducer, initialState), updatedState];
You could reset your setInterval after every state change so that it would see the most up-to-date state. However, this means that your interval could get interrupted a lot.
const otherFunction = useCallback(() => {
fetch('/other_path')
.then(res => {
// why is `state.stateUpdated` here still 'false'????
dispatch({ type: 'DO_SOMETHING_ELSE', res });
});
}
}, [state.stateUpdated]);
useEffect(() => {
const id = setInterval(otherFunction, 30000);
return () => clearInterval(id);
}, [otherFunction]);

Multiple calls to state updater from useState in component causes multiple re-renders

I'm trying React hooks for the first time and all seemed good until I realised that when I get data and update two different state variables (data and loading flag), my component (a data table) is rendered twice, even though both calls to the state updater are happening in the same function. Here is my api function which is returning both variables to my component.
const getData = url => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setLoading(false);
setData(test.data.results);
}
}, []);
return { data, loading };
};
In a normal class component you'd make a single call to update the state which can be a complex object but the "hooks way" seems to be to split the state into smaller units, a side effect of which seems to be multiple re-renders when they are updated separately. Any ideas how to mitigate this?
You could combine the loading state and data state into one state object and then you could do one setState call and there will only be one render.
Note: Unlike the setState in class components, the setState returned from useState doesn't merge objects with existing state, it replaces the object entirely. If you want to do a merge, you would need to read the previous state and merge it with the new values yourself. Refer to the docs.
I wouldn't worry too much about calling renders excessively until you have determined you have a performance problem. Rendering (in the React context) and committing the virtual DOM updates to the real DOM are different matters. The rendering here is referring to generating virtual DOMs, and not about updating the browser DOM. React may batch the setState calls and update the browser DOM with the final new state.
const {useState, useEffect} = React;
function App() {
const [userRequest, setUserRequest] = useState({
loading: false,
user: null,
});
useEffect(() => {
// Note that this replaces the entire object and deletes user key!
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Alternative - write your own state merger hook
const {useState, useEffect} = React;
function useMergeState(initialState) {
const [state, setState] = useState(initialState);
const setMergedState = newState =>
setState(prevState => Object.assign({}, prevState, newState)
);
return [state, setMergedState];
}
function App() {
const [userRequest, setUserRequest] = useMergeState({
loading: false,
user: null,
});
useEffect(() => {
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
This also has another solution using useReducer! first we define our new setState.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
after that we can simply use it like the good old classes this.setState, only without the this!
setState({loading: false, data: test.data.results})
As you may noticed in our new setState (just like as what we previously had with this.setState), we don't need to update all the states together! for example I can change one of our states like this (and it doesn't alter other states!):
setState({loading: false})
Awesome, Ha?!
So let's put all the pieces together:
import {useReducer} from 'react'
const getData = url => {
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null}
)
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setState({loading: false, data: test.data.results})
}
}, [])
return state
}
Typescript Support.
Thanks to P. Galbraith who replied this solution,
Those using typescript can use this:
useReducer<Reducer<MyState, Partial<MyState>>>(...)
where MyState is the type of your state object.
e.g. In our case it'll be like this:
interface MyState {
loading: boolean;
data: any;
something: string;
}
const [state, setState] = useReducer<Reducer<MyState, Partial<MyState>>>(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
Previous State Support.
In comments user2420374 asked for a way to have access to the prevState inside our setState, so here's a way to achieve this goal:
const [state, setState] = useReducer(
(state, newState) => {
newWithPrevState = isFunction(newState) ? newState(state) : newState
return (
{...state, ...newWithPrevState}
)
},
initialState
)
// And then use it like this...
setState(prevState => {...})
isFunction checks whether the passed argument is a function (which means you're trying to access the prevState) or a plain object. You can find this implementation of isFunction by Alex Grande here.
Notice. For those who want to use this answer a lot, I decided to turn it into a library. You can find it here:
Github: https://github.com/thevahidal/react-use-setstate
NPM: https://www.npmjs.com/package/react-use-setstate
Batching update in react-hooks https://github.com/facebook/react/issues/14259
React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like an async call.
This will do:
const [state, setState] = useState({ username: '', password: ''});
// later
setState({
...state,
username: 'John'
});
To replicate this.setState merge behavior from class components,
React docs recommend to use the functional form of useState with object spread - no need for useReducer:
setState(prevState => {
return {...prevState, loading, data};
});
The two states are now consolidated into one, which will save you a render cycle.
There is another advantage with one state object: loading and data are dependent states. Invalid state changes get more apparent, when state is put together:
setState({ loading: true, data }); // ups... loading, but we already set data
You can even better ensure consistent states by 1.) making the status - loading, success, error, etc. - explicit in your state and 2.) using useReducer to encapsulate state logic in a reducer:
const useData = () => {
const [state, dispatch] = useReducer(reducer, /*...*/);
useEffect(() => {
api.get('/people').then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// keep state consistent, e.g. reset data, if loading
else if (status === "loading") return { ...state, data: undefined, status };
return state;
};
const App = () => {
const { data, status } = useData();
return status === "loading" ? <div> Loading... </div> : (
// success, display data
)
}
const useData = () => {
const [state, dispatch] = useReducer(reducer, {
data: undefined,
status: "loading"
});
useEffect(() => {
fetchData_fakeApi().then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
return state;
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// e.g. make sure to reset data, when loading.
else if (status === "loading") return { ...state, data: undefined, status };
else return state;
};
const App = () => {
const { data, status } = useData();
const count = useRenderCount();
const countStr = `Re-rendered ${count.current} times`;
return status === "loading" ? (
<div> Loading (3 sec)... {countStr} </div>
) : (
<div>
Finished. Data: {JSON.stringify(data)}, {countStr}
</div>
);
}
//
// helpers
//
const useRenderCount = () => {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return renderCount;
};
const fetchData_fakeApi = () =>
new Promise(resolve =>
setTimeout(() => resolve({ ok: true, data: { results: [1, 2, 3] } }), 3000)
);
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
PS: Make sure to prefix custom Hooks with use (useData instead of getData). Also passed callback to useEffect cannot be async.
If you are using third-party hooks and can't merge the state into one object or use useReducer, then the solution is to use :
ReactDOM.unstable_batchedUpdates(() => { ... })
Recommended by Dan Abramov here
See this example
A little addition to answer https://stackoverflow.com/a/53575023/121143
Cool! For those who are planning to use this hook, it could be written in a bit robust way to work with function as argument, such as this:
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newState =>
typeof newState == "function"
? setState(prevState => ({ ...prevState, ...newState(prevState) }))
: setState(prevState => ({ ...prevState, ...newState }));
return [state, setMergedState];
};
Update: optimized version, state won't be modified when incoming partial state was not changed.
const shallowPartialCompare = (obj, partialObj) =>
Object.keys(partialObj).every(
key =>
obj.hasOwnProperty(key) &&
obj[key] === partialObj[key]
);
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newIncomingState =>
setState(prevState => {
const newState =
typeof newIncomingState == "function"
? newIncomingState(prevState)
: newIncomingState;
return shallowPartialCompare(prevState, newState)
? prevState
: { ...prevState, ...newState };
});
return [state, setMergedState];
};
In addition to Yangshun Tay's answer you'll better to memoize setMergedState function, so it will return the same reference each render instead of new function. This can be crucial if TypeScript linter forces you to pass setMergedState as a dependency in useCallback or useEffect in parent component.
import {useCallback, useState} from "react";
export const useMergeState = <T>(initialState: T): [T, (newState: Partial<T>) => void] => {
const [state, setState] = useState(initialState);
const setMergedState = useCallback((newState: Partial<T>) =>
setState(prevState => ({
...prevState,
...newState
})), [setState]);
return [state, setMergedState];
};
You can also use useEffect to detect a state change, and update other state values accordingly

Categories

Resources