Using redux's useDispatch in a custom hook yields an error - reactjs

I'm trying to implement useDispatch in a custom hook that dispatches a redux action, but I'm getting the following error:
Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
CODE:
modules file
import { useDispatch, useSelector } from 'react-redux'
export function useFetchEvents() {
const dispatch = useDispatch()
const { items, loading } = useSelector(state => state.events)
if (items.length === 0) {
dispatch(requestEvents(true))
}
}
functional component
import { useFetchEvents } from '../../../modules/events'
const FrontPage = () => {
return(
<div>
Front Page
<button onClick={useFetchEvents}>
Fetch events
</button>
</div>
)
}
export default FrontPage
I've seen the error and read the rules regarding hooks, but if I understand it correctly I should be able to use useDispatch in a custom hook. Like in the following working examples:
https://github.com/mikeour/drinks_drinks_drinks/blob/master/src/hooks/index.js

Then number of hook calls in each invocation should be the same (that's why you are not allowed to call hooks inside if statements).
To achieve this useFetchEvents hook should return a function that can be conditionally called, e.g. onClick
change useFetchEvents like so:
export function useFetchEvents() {
const dispatch = useDispatch()
const { items, loading } = useSelector(state => state.events)
return () => {
if (items.length === 0) {
// Redux action. requestEvents returns object with type.
dispatch(requestEvents(true))
}
}
}
Then in your component do this:
const FrontPage = () => {
const fetchEvents = useFetchEvents()
return(
<div>
Front Page
<button onClick={() => fetchEvents()}>
Fetch events
</button>
</div>
)
}

Related

Cannot update a component (`TodoForm`) while rendering a different component (`TodoTask`). [SOLUTION] [React Redux To-Do App]

WHILE WRITING THIS POST I REALIZED WHAT THE SOLUTION WAS
Every time I dispatch a task to my store the following error occurs:
I have some idea of why it happens. It happens precisely when I try to get the to-do list using useSelector and then mapping through the list. However, the mapping is not the issue but rather returning a react component on the map function. It works just fine if I do not return a functional component and instead use HTML. So the issue, from my POV, is returning a react functional component while passing props to it on a map function.
Here's the code for my home component:
import Input from '../components/Input';
import TodoForm from '../components/TodoForm';
function Home() {
document.title = "MyTodo | Home"
return (
<div className="App">
<h1>MyTodo</h1>
<Input />
<TodoForm />
</div>
);
}
export default Home;
The input component where the action is being dispatched on key down:
import {useState} from 'react'
import { useDispatch } from 'react-redux';
import { todoActions } from '../store/todo';
const Input = () => {
const [inputText, setInputText] = useState("");
const dispatch = useDispatch();
const handleChange = (e) => setInputText(e.target.value)
const handleKeyPress = (event) => {
if (event.code === "Enter") {
// if the expression is false, that means the string has a length of 0 after stripping white spaces
const onlyWhiteSpaces = !inputText.replace(/\s/g, "").length;
!onlyWhiteSpaces &&
dispatch(
todoActions.addTask({ label: inputText, done: false })
);
setInputText("");
}
};
return (
<input
type="text"
onKeyDown={(e) => handleKeyPress(e)}
onChange={(e) => handleChange(e)}
value={inputText}
/>
);
}
export default Input
The TodoForm where I am using useSelector to get the todo list from the redux store and mapping thru it:
import { useSelector } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import TodoTask from "./TodoTask";
const TodoForm = () => {
const tasks = useSelector((state) => state.todo.taskList);
const renderedListItems = tasks.map((task, index) => {
return (
<TodoTask
key={uuidv4()}
task={task}
targetIndex={index}
/>
);
});
return <div className="container">{renderedListItems}</div>;
};
export default TodoForm;
Finally the TodoTask component which is the child component being returned on the map function above:
import { useDispatch } from "react-redux";
import { todoActions } from "../store/todo";
const TodoTask = ({ task, targetIndex }) => {
const {text, done} = task;
console.log("Task: ", task);
const dispatch = useDispatch()
const removeTask = dispatch(todoActions.deleteTask(targetIndex))
return (
<div
className="alert alert-primary d-flex justify-content-between"
role="alert"
>
{text}
<button type="button" className="btn-close" onClick={()=>removeTask}></button>
</div>
);
};
export default TodoTask;
This is my first time facing this issue, and I know it has something to do with redux and how the useSelector hook forces a component to re-render. So the useSelector is re-rendering the TodoForm component, and since we are mapping and returning another component, that component is also being rendered simultaneously. At least, that is how I understand it. Let me know if I am wrong.
Things I have tried:
Wrapping the TodoTask in React.memo. Saw it somewhere as a possible solution to this kind of issue, but that did not work.
Passing shallowEqual as a second parameter on the TodoForm useSelector. This does prevent the page from going into an infinity loop, but the tasks show up empty but are being added to the redux store. However, with this method, the first warning stills shows up, and the console log in the TodoTask component does not execute.
Passing shallowEqual as a second parameter on the TodoForm useSelector. This does prevent the page from going into an infinity loop but the tasks show up empty but are being added to the redux store. However, with this method, the first warning stills shows up and the console log in the TodoTask component does not execute.
I realized what I was doing wrong while writing this part. The console log in the TodoTask component was working, but I had the browser console filtering for errors only. When I check the messages section, I saw everything working fine. Then when I checked the Task component, I noticed I was trying to read a property that did not exist and hence why the tasks had no text.
In other words, the solution was adding shallowEqual as second parameter of the useSelector hook in my TodoForm component that was the one mapping thru the todo tasks array. As I said, useSelector forces a component to re-render. shallowEquals checks if the existing state isn't the same as we already had and avoids unnecessary re-renders, which can lead my application to exceed the maximum update length.
Code fix [Solution]:
import { memo } from "react";
import { shallowEqual, useSelector } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import TodoTask from "./TodoTask";
const TodoForm = () => {
// shallowEqual prevents unnecessary re-renders which can lead to an infinite loop
// it compares the current state with the previous one, if they are the same, it does not re-render the component
const tasks = useSelector((state) => state.todo.taskList, shallowEqual);
const renderedListItems = tasks.map((task, index) => {
return (
<TodoTask
key={uuidv4()}
task={task}
targetIndex={index}
/>
);
});
return <div className="container">{renderedListItems}</div>;
};
export default memo(TodoForm);
Honestly, I have been stuck on this since yesterday and I cannot believe I realize the solution just when I was about to ask for help. Hope this helps anyone else who faces a similar issue in the future.

Invalid hook call error when showing/hiding component with React Redux

I'm attempting to create a React/Redux component that shows/hides an element when clicked.
I'm using this to trigger the function from another component:
import React from 'react'
//Some other code...
import { useSelector } from 'react-redux'
import onShowHelpClicked from '../help/AddHelpSelector'
<button onClick={onShowHelpClicked}>Help</button>
This this is AddHelpSelector:
import { useState } from 'react'
import { useDispatch } from 'react-redux'
import { helpVisible } from './HelpSlice'
export const AddHelp = () => {
const [isVisible, showHelp] = useState('')
const dispatch = useDispatch()
const onShowHelpClicked = () => {
dispatch(
helpVisible({
isVisible,
})
)
if (isVisible) {
showHelp(false)
} else {
showHelp(true)
}
}
return (
<section>
<h2 style={{ visibility: { isVisible } }}>Help section</h2>
</section>
)
}
export default AddHelp
Finally, this is HelpSlice
import { createSlice } from '#reduxjs/toolkit'
const initialState = [{ isVisible: false }]
const helpSlice = createSlice({
name: 'help',
initialState,
reducers: {
helpVisible(state, action) {
state.push(action.payload)
},
},
})
export const { helpVisible } = helpSlice.actions
export default helpSlice.reducer
I'm fairly certain I'm doing multiple things wrong, as this is my first attempt to do anything with Redux and I'm still struggling to wrap my mind around it after a week of learning.
But specifically, when clicking the help button I get this error.
"Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app"
The linked documentation provides a way to test a component to see if React is importing properly, and it's not. But I'm not really sure what I'm doing wrong.
I think it may be that I'm importing React multiple times, but if I don't then I can't use "useState."
What's the correct way to do this? I'm open to corrections on both my code as well as naming conventions. I'm using boilerplate code as a template as I try to understand this better after getting through the documentation as well as Mosh's 6 hour course which I just finished.
You're importing the < AddHelpSelector /> component here import onShowHelpClicked from '../help/AddHelpSelector', and then you try to use it as a callback handler for the button's onClick, which doesn't really make sense. I assume you actually wanted to only import the onShowHelpClicked function declared inside the < AddHelpSelector /> component (which is not really a valid way of doing it). Since you want to control the visibility using redux state, you could just grab the flag from the redux store inside the < AddHelpSelector /> component using useSelector hook. To set it, you're gonna do that in the component where your button is. For that, you just need to dispatch an action(like you already did), with the updated flag. No need for the local useState. Also, using the flag you could just conditionally render the element.
const App = () => {
const dispatch = useDispatch();
const { isVisible } = useSelector((state) => ({ isVisible: state.isVisible }));
const handleClick = () => {
dispatch(
helpVisible({
!isVisible,
})
)
}
return (<button onClick={handleClick}>Help</button>);
}
export const AddHelp = () => {
const { isVisible } = useSelector((state) => ({ isVisible: state.isVisible }));
return (
<section>
{isVisible && <h2>Help section</h2>}
</section>
)
}
export default AddHelp

how to send params in react.js using useNavigate in react-router-dom version 6

import React from "react";
import Detailpost from "../../DetailPost/detailpost";
const Post = (props) => {
return (
<div className="post">
<div className="img-thum">
<img src="https://placeimg.com/200/150/tech" alt="IMG" />
</div>
<div className="content">
<div
className="title"
onClick={()=>props.Detail(props.data.id)}
>
{props.data.title}
</div>
<div className="Body">{props.data.body}</div>
<div className="rem">
<button
className="remove"
onClick={() => props.remove(props.data.id)}
>
Remove
</button>
<button
className="update"
onClick={() => props.update(props.data)}
>
Update
</button>
</div>
</div>
);
}
export default Post;
In this case I'll make when I click the title I move to another page.
handleDetail = (id) => {
const navigate = useNavigate();
navigate('/detail-post', { UserId: id })
}
but when I run the code I get some problem:
Uncaught Error: Invalid hook call. Hooks can only be called inside of
the body of a function component.
anyone can help me..
Issues
You are breaking the rules of hooks by conditionally call the useNavigate hook.
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top
level of your React function, before any early returns. By following
this rule, you ensure that Hooks are called in the same order each
time a component renders. That’s what allows React to correctly
preserve the state of Hooks between multiple useState and useEffect
calls.
You are not passing the state correctly. The navigate function takes a second argument options object with state and replace keys. Any data you want to send along with the route transition should be on the state key.
useNavigate
declare function useNavigate(): NavigateFunction;
interface NavigateFunction {
(
to: To,
options?: { replace?: boolean; state?: any }
): void;
(delta: number): void;
}
Solution
import { useNavigate } from 'react-router-dom';
...
const navigate = useNavigate();
...
handleDetail = (UserId) => {
navigate('/detail-post', { state: { UserId } });
}
...
And OFC use the useLocation hook on the receiving route to access the passed route state.
import { useLocation } from 'react-router-dom';
...
const { state } = useLocation();
const { UserId } = state || {};
...
useNavigate is a hook, and as a rule, only call hooks at the top level of your React component
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns
const YourComponent = () => {
const navigate = useNavigate();
const handleDetail = (id) => {
navigate('/detail-post', {UserId:id})
}
// ...
}

Invalid hook call. Hooks can only be used inside of a react function component... useEffect, redux

I encountered a problem with my react application related to hooks. Technologies being used: React, Redux, Apollo, ChakraUI.
Here is the React component that is troubling me:
import React, { useEffect } from "react";
import { Flex, Container, Heading, Text } from "#chakra-ui/react";
import { connect, useSelector, useDispatch } from "react-redux";
import { State } from "../state/store";
import { fetchRecipes } from "../state/recipe/actions";
interface RecipesListProps {}
const RecipesList: React.FC<RecipesListProps> = ({}) => {
const recipes = useSelector<State>(
(state) => state.recipe.recipes
) as State["recipe"]["recipes"];
const loading = useSelector<State>(
(state) => state.recipe.loading
) as State["recipe"]["loading"];
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchRecipes());
}, []);
if (loading) {
return <h1>Loading....</h1>;
}
return (
<Flex
m="auto"
mt="5rem"
w="50%"
direction="column"
justifyContent="center"
alignItems="center"
>
<Heading>Your Recipes</Heading>
<Flex mt="2rem" direction="column" w="100%" padding="0" gridGap="2rem">
{recipes &&
recipes.map((recipe) => (
<Container
key={recipe.id}
bg="orange.100"
borderRadius="0.2rem"
padding="1rem"
maxW="100%"
>
<Text fontSize="xl" fontWeight="bold">
{recipe.title}
</Text>
<Text>{recipe.description}</Text>
</Container>
))}
</Flex>
</Flex>
);
};
export default RecipesList;
Notice the use of the useEffect() hook. This is the error I am getting:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
I am pretty sure I am disobeying rule number, 2 i.e. I am breaking the Rules of Hooks. As soon as I take the useEffect() call out of the component, it doesn't throw an error.
Could someone please give some guidance as to what I am doing wrong?
Thanks.
Edit:
The fetchRecipes function is a Redux thunk function that fetches recipes from a graphql server
Update:
I have been hacking away at a solution to this problem. I replaced the dispatch(fetchRecipes()) call with a console.log("hello world"), and it worked perfectly!
This is boggling my mind! Is this a problem with the fetchRecipes function?
Edit:
Here's the code for the fetchRecipes function:
export const fetchRecipes = () => {
return (dispatch: Dispatch) => {
dispatch(fetchRecipesPending());
const { data } = useRecipesQuery();
const errors = data?.recipes.errors;
const recipes = data?.recipes.recipes;
if (errors?.length) {
dispatch(fetchRecipesFailure(errors));
} else {
dispatch(fetchRecipesSuccess(recipes));
}
};
};
useRecipesQuery is a custom hook that was auto generated using the graphql-codegen library. It builds up on the useQuery hook from the #apollo/client library.
Your useEffect needs a little rewrite. You are dispatching the function fetchRecipes which in itself is a hook, but the thing dispatched should be a plain "action" (using Redux terminology here). So I guess we can fix that by breaking up your fetchRecipes fn.
A snippet of the component would now look like following:
const { data } = useRecipesQuery();
useEffect(() => {
if (!data) {
dispatch(fetchRecipesPending()) // I only assume you fetch on render
}
if (data?.recipes?.errors) {
dispatch(fetchRecipesFailure(data?.recipes.errors)
}
if (data?.recipes?.recipes) {
dispatch(fetchRecipesSuccess(data?.recipes?.recipes)))
}
}, [data]);
Now it should be fine AND more readable. Either way, as some has already suggested, I would think about using some more standardised way like using Redux w/ Thunks or Sagas, or, even better - I see you might be doing a GQL query - if so, just use a hook for it and handle the data with Apollo Client.

Can't map array with useEffect and React Redux

I am trying to map over an employee list after useEffect fetches the array and I dispatch it to my redux-state. If I console log it I see an array full of objects, but my component doesn't appear to be re-rendering. Any suggestions?
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { setEmployees } from '../redux/reducers/employeeListReducer'
import employeesService from '../back-end/services/employees'
const EmployeeList = ({ }) => {
const dispatch = useDispatch()
const employees = useSelector(state => state.employeeList)
const user = useSelector(state => state.user)
employeesService.setToken(user.token)
useEffect(() => {
(async () => {
const employeeList = await employeesService.getAll()
dispatch(setEmployees(employeeList))
})();
}, [dispatch])
if (employees === null) {
return <p>Loading Employees...</p>
}
return (
<div>
{console.log(employees)}
{employees.map(e =>
<div key={e.id}>
{e.name} {e.phone} {e.email}
</div>
)}
</div>
)
}
export default EmployeeList
It is unclear from the description what may be causing the first issue you describe (there is no console.log we can look at to determine why there is a discrepancy), but you cannot call hooks from within callbacks provided to other hooks. This is a limitation of the how hooks are implemented. They depend on always being called in the same order (so you cannot conditionally call a hook function), and they must all have been called by the time your function returns. Details can be found here:
https://reactjs.org/warnings/invalid-hook-call-warning.html
Basically, you must call hook functions within the top level of a functional component, or within the top level of another hook (for instance, you will see hooks that are implemented using other hooks). All hook functions must have been called by the time your function returns. If the hook is called within a callback, it very likely will be called after your function returns. So React warns you about this.
All of that said, moving your declaration of dispatch to the top level of your component should resolve this issue:
import React, {useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { setEmployees} from '../redux/reducers/employeeListReducer'
import employeesService from '../back-end/services/employees'
const EmployeeList = ({ employee }) => {
const dispatch = useDispatch()
const employees = useSelector(state => state.employeeList)
const user = useSelector(state => state.user)
employeesService.setToken(user.token)
useEffect(() => {
employeesService
.getAll()
.then(employeeList => {
dispatch(setEmployees(employeeList))
})
}, [dispatch])
return (
<div>
Employee List Placeholder
{employees.map(e =>
<div key={e.id}>
{e.name} {e.phone} {e.email}
</div>
)}
</div>
)
}
export default EmployeeList

Resources