How to use selectors and dispatch with a custom hook? - javascript

I'm trying to dispatch a Redux action from a custom hook with useDispatch but I'm getting an "Invalid hook call" error. If I'm understanding the rules of hooks correctly, I don't think I'm breaking any of the rules as I'm calling useDispatch from a custom hook. What am I missing??
CODE
Main selector for functional component
export const getFlattenedDevicesList = createSelector(
getDevicesList,
(deviceList) => {
return (
deviceList
.map(makeOrganizationKey)
.map(makeProjectKey)
.map(makeDeviceProfileKey)
.map(DispatchLSU) // custom hook
);
}
);
Custom hook
export function DispatchLSU(resource) {
const dispatch = useDispatch();
console.log(resource.device_id);
const deviceId = resource.device_id;
// useEffect(() => {
// dispatch(getHealthLsu(deviceId));
// });
dispatch(getHealthLsu(deviceId));
const result = {
...resource,
};
return result;
};
The goal is to dispatch the action for each entry in deviceList then insert new data from API call.

Hooks rule: Only Call Hooks from React Functions
This code is breaking hooks rules, Only Call Hooks from React Functions.
You are probably calling getFlattenedDevicesList with useSelector then inside of this you call DispatchLSU who call useDispatch.
So you have a hook inside a hook that takes a callback:
useSelector -> getFlattenedDevicesList -> DispatchLSU -> useDispatch
How to call a selector and dispatch with hooks ?
If you want to call dispatch and your selector inside a hook. Here is how to achieve it
export function DispatchLSU() {
const dispatch = useDispatch();
const resource = useSelector(getFlattenedDevicesList);
const deviceId = resource.device_id;
useEffect(() => {
dispatch(getHealthLsu(deviceId));
}, []);
const result = {
...resource,
};
return result;
};
Additional notes
Convention is to name custom hooks with useXXX
Instead of dispatching inside the hook method, you can use useDispatch or useCallback to avoid making a dispatch when you render
Instead of having a selector that return all resources you can have another one that return only the deviceId

Your issue is that getFlattenedDevicesList is a normal function and a normal function can not call a hook. Hooks can only be called from the top level of either 1. a react component or 2. another hook. You can convert getFlattenedDevicesList to a hook by using another hook inside it, but then you can only do so at the top level.
i.e.
const getFlattenedDevicesList = () => {
// you can only use hooks here
if (true) {
// you can't use hooks here
// or anywhere that's not at top level of getFlattenedDevicesList
}
}
The entrypoint or root of where you start calling that hook from, along with the entire hook chain it may or may not be calling, must be either a component or a hook.
Example:
const example = () => {}
That is a normal function, not a hook.
const useExample = () => {}
That is a normal JS function, not a hook.
const useExample = () => {
const [value, setValue] = useState()
}
That's a custom hook since it's using another hook, react will figure out that it needs to be a hook as well.
const example = () => {
const [value, setValue] = useState()
}
That's also exactly the same custom hook as above and will work exactly the same as the hook above, it's just called different.
If you want to chain hooks:
const useHook1 = () => {
const [value, setValue] = useState()
return {value, setValue}
}
const useHook2 = () => {
const {value, setValue} = useHook1()
return {value}
}
That's valid because any hook can also call another hook, just like you did when using useState.
However if you now want to use useHook2 somewhere that has to be called from another hook or a functional component.
i.e.
const Component = (): JSX.Element => {
const {value} = useHook2()
}
Is valid.
However if you try to call useHook1 or useHook2 from any normal function or anything that's not a hook or a function component it will break.
To fix it you can do something like:
const useDispatch = () => {
const dispatch = (resource) => {
// do whatever here
}
return dispatch
}
const useExample = () => {
const dispatch = useDispatch()
dispatch(// resource)
}
Note: The naming of the hook is irrelevant, you can call it anything you want. React will figure out it's a hook based on the fact that it's using other hooks or something like useState. However it is strongly recommended that you do prefix it with use, i.e. useSomeHook, because 1. it makes it easy for other developers to see what is a hook and what's a normal function and 2. a lot of tools like eslint is built around a use prefix in order to apply hook rules and validations so if you don't name it with "use" those things will not work correctly.

Related

ReactJS Hooks dependency an async operation

So I have a hook that on mount, reads data from an indexedDB and stores that data in its internal state
The problem that I have, is that the indexedDB data gets changed from another component (added/removed), and I need to react to those changes in this hook. I'm not 100% familiar with hooks and how this would be done, but the first thought would be to have as hook dependency the value from the indexedDB.
HOWEVER, the reading of the data from the indexedDB is an async operation and the hook dependency would be a.. promise.
So basically, the flow is as follows:
Component 1 calls the hook like so:
const eventItems = useEventListItems({
sortBy,
sortGroupedBy,
eventTimestamp,
events,
assets,
touchedEventIds,
unsyncedEvents, // <--- this is the one that we need
order,
});
The useEventListItems hook, on mount, reads the data from the indexed DB, stores it in its internal state and returns it:
const { readUnsyncedEvents } = useDebriefStore();
const [unsyncedEvents, setUnsyncedEvents] = useState<number[]>([]);
useEffectAsync(async () => {
const storedUnsyncedEventIds = await readUnsyncedEvents<number[]>();
if (storedUnsyncedEventIds?.data) {
setUnsyncedEvents(storedUnsyncedEventIds.data);
}
}, [setUnsyncedEvents]);
where readUnsyncedEvents is:
export const readUnsyncedEvents = <T>(type: Type): Promise<DebriefStoreEntry<T>> =>
debriefStore
.get(type)
.then((entry) => entry && { data: entry.data, timestamp: entry.timestamp });
The unsyncedEvents from the indexedDB are then changed from another component.
What should happen now, is that the useEventListItems hook should listen to the changes in the IDB and update the unsyncedEvents in its internal state, passing them to the component that uses this hook. How would I achieve this?
My first thought was to have something like this in the useEventListItems hook:
useEffect(() => {
setUnsyncedEvents(..newValueFromIdb);
}, [ await readUnsyncedEvents()]);
but that won't work since it'll be a promise. Is there anyway I can have as hook dependency, a value returned by an async operation?
You can use Context API to refetch the data from IDB.
The idea here is to create a context with a counter variable which will be updated after each IDB update operation. And useEventListItems hook will read that counter variable from context and trigger the useEffect hook.
export const IDBContext = React.createContext({
readFromIDB: null,
setReadFromIDB: () => {}
});
export const IDBContextProvider = ({ children }) => {
const [readFromIDB, setReadFromIDB] = useState(0);
return (
<IDBContext.Provider value={{ readFromIDB, setReadFromIDB }}>
{children}
</IDBContext.Provider>
);
};
This is how your useEventListItems will look like.
const { readUnsyncedEvents } = useDebriefStore();
const [unsyncedEvents, setUnsyncedEvents] = useState<number[]>([]);
const {readFromIDB} = useContext(IDBContext); // this variable will be updated after each IDB update.
useEffectAsync(async () => {
const storedUnsyncedEventIds = await readUnsyncedEvents<number[]>();
if (storedUnsyncedEventIds?.data) {
setUnsyncedEvents(storedUnsyncedEventIds.data);
}
}, [readFromIDB,setUnsyncedEvents]); // added that to dependency array to trigger the hook on value change.
And here are the components:
const IDBUpdateComponent = ()=>{
const {readFromIDB,setReadFromIDB} = useContext(IDBContext);
const updateIDB = ()=>{
someIDBUpdateOpetation().then(res=>{
setReadFromIDB(readFromIDB+1) // update the context after IDB update is successful.
}).catch(e=>{})
}
return(
<div>IDBUpdateComponent</div>
);
}
const IDBConsumerComponent = ()=>{
return (
<div>IDBConsumerComponent</div>
)
}
Just make sure that both the components are wrapped inside the context so that they can access the values.
const App = ()=>{
return(
<div>
<IDBContextProvider>
<IDBUpdateComponent />
<IDBConsumerComponent />
</IDBContextProvider>
</div>
)
}

Why does my useEffect react function run when the page loads although I am giving it a second value array

Why is my useEffect react function running on every page load although giving it a second value array with a query variable?
useEffect( () => {
getRecipes();
}, [query]);
Shouldn't it only run when the query state variable changes? I have nothing else using the getRecipes function except of the useEffect function.
import React, {useEffect, useState} from 'react';
import './App.css';
import Recipes from './components/Recipes/Recipes';
const App = () => {
// Constants
const APP_ID = '111';
const APP_KEY = '111';
const [recipes, setRecipes] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('');
const [showRecipesList, setShowRecipesList] = useState(false);
// Lets
let recipesList = null;
// Functions
useEffect( () => {
getRecipes();
}, [query]);
// Get the recipie list by variables
const getRecipes = async () => {
const response = await fetch(`https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}&from=0&to=3&calories=591-722&health=alcohol-free`);
const data = await response.json();
console.log(data.hits);
setRecipes(data.hits);
}
// Update the search constant
const updateSearch = e => {
console.log(e.target.value);
setSearch(e.target.value);
}
const runQuery = e => {
e.preventDefault();
setQuery(search);
}
// List recipes if ready
if (recipes.length) {
console.log(recipes.length);
recipesList = <Recipes recipesList={recipes} />
}
return (
<div className="App">
<form className='search-app' onSubmit={ runQuery }>
<input
type='text'
className='search-bar'
onChange={ updateSearch }
value={search}/>
<button
type='submit'
className='search-btn' > Search </button>
</form>
<div className='recipesList'>
{recipesList}
</div>
</div>
);
}
export default App;
Following this: https://www.youtube.com/watch?v=U9T6YkEDkMo
A useEffect is the equivalent of componentDidMount, so it will run once when the component mounts, and then only re-run when one of the dependencies defined in the dependency array changes.
If you want to call getRecipes() only when the query dependency has a value, you can call it in a conditional like so:
useEffect(() => {
if(query) {
getRecipes()
}
}, [query])
Also, as your useEffect is calling a function (getRecipes) that is declared outside the use effect but inside the component, you should either move the function declaration to be inside the useEffect and add the appropriate dependencies, or wrap your function in a useCallback and add the function as a dependency of the useEffect.
See the React docs for information on why this is important.
UseEffect hook work equivalent of componentDidMount, componentDidUpdate, and componentWillUnmount combined React class component lifecycles.but there is a different in time of acting in DOM.componentDidMount and useEffect run after the mount. However useEffect runs after the paint has been committed to the screen as opposed to before. This means you would get a flicker if you needed to read from the DOM, then synchronously set state to make new UI.useLayoutEffect was designed to have the same timing as componentDidMount. So useLayoutEffect(fn, []) is a much closer match to componentDidMount() than useEffect(fn, []) -- at least from a timing standpoint.
Does that mean we should be using useLayoutEffect instead?
Probably not.
If you do want to avoid that flicker by synchronously setting state, then use useLayoutEffect. But since those are rare cases, you'll want to use useEffect most of the time.

How can we use Redux dispatch() into useEffect? cause I am getting error [duplicate]

My folder structure:
|--App
|--Components
|--PageA.js
|--PageB.js
|--PageC.js
|--common-effects
|--useFetching.js
I am refactoring my code to fetch data from API, using react hooks.
I want to dispatch an action from useEffect in useFetching.js that is intercepted by saga middleware. The action should be dispatched only when the components(PageA, PageB, PageC) mount.
I am using redux, react-redux and redux-saga.
PageA.js:
function(props) {
useFetching(actionParams)
//....//
}
Similar code for PageB and PageC components.
I have abstracted the reusable code to fetch data in useFetching Custom hook.
useFetching.js
const useFetching = actionArgs => {
useEffect( () => {
store.dispatch(action(actionArgs)); // does not work
})
}
I don't know how to access redux dispatch in useFetching. I tried it with useReducer effect, but the sagas missed the action.
Version using react-redux hooks:
You can even cut out the connect function completely by using useDispatch from react-redux:
export default function MyComponent() {
useFetching(fetchSomething);
return <div>Doing some fetching!</div>
}
with your custom hook
import { useDispatch } from 'react-redux';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
Edit: removed dispatch from custom hook as suggested by #yonga-springfield
Note: React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
You would need to pass either bound action creators or a reference to dispatch to your hook. These would come from a connected component, same as you would normally use React-Redux:
function MyComponent(props) {
useFetching(props.fetchSomething);
return <div>Doing some fetching!</div>
}
const mapDispatch = {
fetchSomething
};
export default connect(null, mapDispatch)(MyComponent);
The hook should then call the bound action creator in the effect, which will dispatch the action accordingly.
Also, note that your current hook will re-run the effect every time the component is re-rendered, rather than just the first time. You'd need to modify the hook like this:
const useFetching = someFetchActionCreator => {
useEffect( () => {
someFetchActionCreator();
}, [])
}
This is just to bring some optimization to #Alex Hans' answer.
As per the documentation here. A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.
With this in mind, we need not send a reference to the dispatch function to the useFetching hook as a parameter but rather, simply not send it and rather simply use it from within the useFetching hook with the appropriate imports.
Here's an excerpt of what I mean.
import { useDispatch } from 'react-redux';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
I can't ascertain this example will fit without errors in your codebase in your case but just trying to explain the idea/concept behind this post.
Hope this helps any future comer.
Alex Hans right decision with dispatch, but to eliminate request loops to api you can specify the dependence on dispatch ( I used Redux Toolkit )
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import axios from 'axios'
import { getItemsStart, getItemsSuccess, getItemsFailure } from '../features/itemsSlice'
const fetchItems = () => async dispatch => {
try {
dispatch(getItemsStart());
const { data } = await axios.get('url/api')
dispatch(getItemsSuccess(data))
} catch (error) {
dispatch(getItemsFailure(error))
}
}
const PageA = () => {
const dispatch = useDispatch()
const { items } = useSelector(state => state.dataSlice)
useEffect(() => {
dispatch(fetchItems())
}, [dispatch])
return (
<ul>
{items.map(item => <li>{item.name}</li>}
</ul>
)
}
export default PageA
it is important to passed dependency parameter of dispatch in the useEffect(() => {...}, [dispatch])
useEffect(() => {
fetchData();
}, []);
async function fetchData() {
try {
await Auth.currentSession();
userHasAuthenticated(true);
} catch (e) {
if (e !== "No current user") {
alert(e);
}
}
dispatch(authentication({ type: "SET_AUTHING", payload: false }));
}

React Custom Hook Problem - Infinite Dependency Loop

I've been loving getting into hooks and dealing with all the new fun issues that come up with real-world problems :) Here's one I've run into a couple of times and would love to see how you "should" solve it!
Overview: I have created a custom hook to capsulate some of the business logic of my app and to store some of my state. I use that custom hook inside a component and fire off an event on load.
The issue is: my hook's loadItems function requires access to my items to grab the ID of the last item. Adding items to my dependency array causes an infinite loop. Here's a (simplified) example:
Simple ItemList Component
//
// Simple functional component
//
import React, { useEffect } from 'react'
import useItems from '/path/to/custom/hooks/useItems'
const ItemList = () => {
const { items, loadItems } = useItems()
// On load, use our custom hook to fire off an API call
// NOTE: This is where the problem lies. Since in our hook (below)
// we rely on `items` to set some params for our API, when items changes
// `loadItems` will also change, firing off this `useEffect` call again.. and again :)
useEffect(() => {
loadItems()
}, [loadItems])
return (
<ul>
{items.map(item => <li>{item.text}</li>)}
</ul>
)
}
export default ItemList
Custom useItems Hook
//
// Simple custom hook
//
import { useState, useCallback } from 'react'
const useItems = () => {
const [items, setItems] = useState([])
// NOTE: Part two of where the problem comes into play. Since I'm using `items`
// to grab the last item's id, I need to supply that as a dependency to the `loadItems`
// call per linting (and React docs) instructions. But of course, I'm setting items in
// this... so every time this is run it will also update.
const loadItems = useCallback(() => {
// Grab our last item
const lastItem = items[items.length - 1]
// Supply that item's id to our API so we can paginate
const params = {
itemsAfter: lastItem ? lastItem.id : nil
}
// Now hit our API and update our items
return Api.fetchItems(params).then(response => setItems(response.data))
}, [items])
return { items, loadItems }
}
export default useItems
The comments inside the code should point out the problem, but the only solution I can come up with right now to make linters happy is to supply params TO the loadItems call (ex. loadItems({ itemsAfter: ... })) which, since the data is already in this custom hook, I am really hoping to not have to do everywhere I use the loadItems function.
Any help is greatly appreciated!
Mike
If you plan to run an effect just once, omit all dependencies:
useEffect(() => {
loadItems();
}, []);
You could try with useReducer, pass the dispatch as loadItems as it never changes reference. The reducer only cares if the action is NONE because that is what the cleanup function of useEffect does to clean up.
If action is not NONE then state will be set to last item of items, that will trigger useEffect to fetch using your api and when that resolves it'll use setItems to set the items.
const NONE = {};
const useItems = () => {
const [items, setItems] = useState([]);
const [lastItem, dispatch] = useReducer(
(state, action) => {
return action === NONE
? NONE
: items[items.length - 1];
},
NONE
);
useEffect(() => {
//initial useEffect or after cleanup, do nothing
if (lastItem === NONE) {
return;
}
const params = {
itemsAfter: lastItem ? lastItem.id : Nil,
};
// Now hit our API and update our items
Api.fetchItems(params).then(response =>
setItems(response)
);
return () => dispatch(NONE); //clean up
}, [lastItem]);
//return dispatch as load items, it'll set lastItem and trigger
// the useEffect
return { items, loadItems: dispatch };
};

React hooks: dispatch action from useEffect

My folder structure:
|--App
|--Components
|--PageA.js
|--PageB.js
|--PageC.js
|--common-effects
|--useFetching.js
I am refactoring my code to fetch data from API, using react hooks.
I want to dispatch an action from useEffect in useFetching.js that is intercepted by saga middleware. The action should be dispatched only when the components(PageA, PageB, PageC) mount.
I am using redux, react-redux and redux-saga.
PageA.js:
function(props) {
useFetching(actionParams)
//....//
}
Similar code for PageB and PageC components.
I have abstracted the reusable code to fetch data in useFetching Custom hook.
useFetching.js
const useFetching = actionArgs => {
useEffect( () => {
store.dispatch(action(actionArgs)); // does not work
})
}
I don't know how to access redux dispatch in useFetching. I tried it with useReducer effect, but the sagas missed the action.
Version using react-redux hooks:
You can even cut out the connect function completely by using useDispatch from react-redux:
export default function MyComponent() {
useFetching(fetchSomething);
return <div>Doing some fetching!</div>
}
with your custom hook
import { useDispatch } from 'react-redux';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
Edit: removed dispatch from custom hook as suggested by #yonga-springfield
Note: React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
You would need to pass either bound action creators or a reference to dispatch to your hook. These would come from a connected component, same as you would normally use React-Redux:
function MyComponent(props) {
useFetching(props.fetchSomething);
return <div>Doing some fetching!</div>
}
const mapDispatch = {
fetchSomething
};
export default connect(null, mapDispatch)(MyComponent);
The hook should then call the bound action creator in the effect, which will dispatch the action accordingly.
Also, note that your current hook will re-run the effect every time the component is re-rendered, rather than just the first time. You'd need to modify the hook like this:
const useFetching = someFetchActionCreator => {
useEffect( () => {
someFetchActionCreator();
}, [])
}
This is just to bring some optimization to #Alex Hans' answer.
As per the documentation here. A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.
With this in mind, we need not send a reference to the dispatch function to the useFetching hook as a parameter but rather, simply not send it and rather simply use it from within the useFetching hook with the appropriate imports.
Here's an excerpt of what I mean.
import { useDispatch } from 'react-redux';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
I can't ascertain this example will fit without errors in your codebase in your case but just trying to explain the idea/concept behind this post.
Hope this helps any future comer.
Alex Hans right decision with dispatch, but to eliminate request loops to api you can specify the dependence on dispatch ( I used Redux Toolkit )
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import axios from 'axios'
import { getItemsStart, getItemsSuccess, getItemsFailure } from '../features/itemsSlice'
const fetchItems = () => async dispatch => {
try {
dispatch(getItemsStart());
const { data } = await axios.get('url/api')
dispatch(getItemsSuccess(data))
} catch (error) {
dispatch(getItemsFailure(error))
}
}
const PageA = () => {
const dispatch = useDispatch()
const { items } = useSelector(state => state.dataSlice)
useEffect(() => {
dispatch(fetchItems())
}, [dispatch])
return (
<ul>
{items.map(item => <li>{item.name}</li>}
</ul>
)
}
export default PageA
it is important to passed dependency parameter of dispatch in the useEffect(() => {...}, [dispatch])
useEffect(() => {
fetchData();
}, []);
async function fetchData() {
try {
await Auth.currentSession();
userHasAuthenticated(true);
} catch (e) {
if (e !== "No current user") {
alert(e);
}
}
dispatch(authentication({ type: "SET_AUTHING", payload: false }));
}

Categories