In my simple react app there two component: App and Comments.
App.js =>
import Comments from "./comments/Comments";
const App = () => {
return (
<div>
<h1>App.js</h1>
<Comments currentUserId="1" />
</div>
);
};
export default App;
Comments.js =>
import { useState, useEffect } from "react";
import { getComments as getCommentsApi } from "../api";
const Comments = ({ currentUserId }) => {
const [comments, setComments] = useState([]);
console.log("Comments", comments);
useEffect(() => {
getCommentsApi()
.then(data => {
setComments(data);
})
}, []);
return (
<div>
<h1>Comments.js</h1>
</div>
);
};
export default Comments;
If you take a look at my Comments Component, there im logging the Comments in the function body and it should be execute two time, first for the initial load and the second time due to the useEffect and changing the state.
React Components Should Re-execute when props or state changed
But 4 logs appear in my console. Why?
I think you are using strict Mode, that's why you are getting the unexpected result. Remove the Strict Mode component from the index.js file and the code will work as expected
Related
I am learning Redux and I have deviated from the instructor's code. I am trying to convert my code from context & state into Redux.
Is it advisable to use setReduxObject (setCategoriesMap in my code) and selectReduxObject (selectCategoriesMap in my code) in the same .jsx page? Are there any concerns around this?
Thanks!
My code:
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getCategoriesAndDocuments } from "../../utils/firebase/firebase.utils";
import { setCategoriesMap } from "../../store/categories/categories.action";
import { selectCategoriesMap } from "../../store/categories/categories.selector";
import Category from "../../components/category/category.component";
const Shop = () => {
const dispatch = useDispatch();
useEffect(() => {
const getCategoriesMap = async () => {
const categories = await getCategoriesAndDocuments();
dispatch(setCategoriesMap(categories));
};
getCategoriesMap();
}, []);
const categoriesMap = useSelector(selectCategoriesMap);
return (
<div>
{Object.keys(categoriesMap).map((key) => {
const products = categoriesMap[key];
return <Category key={key} title={key} products={products} />;
})}
</div>
);
};
export default Shop;
This is just the default approach, nothing to be concerned about.
As soon as you're using getCategoriesAndDocuments the same way in another component though, it's better to move this to an async action creator.
Could even do it for this component already to improve separation of concerns. The component does not necessarily need to be involved with firebase, its job is display logic. Wether the data comes from firebase or localStorage or some graphQL server should not matter.
I found that the issue is stemming from a Higher Order Component that wraps around a react-router-dom hook.
This Higher Order Component is imported from #auth0/auth0-react and is a requirement in our project to handle logging out with redirect.
However, even just a basic HOC, the issue is persisting.
in my App.js file, I have a react-redux provider. And inside the provider I have a ProtectLayout component.
ProtectLayout checks for an error reducer, and if the error property in the reducer has a value, it sets a toast message, as seen below.
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import PageLoader from "../loader/PageLoader";
import { useToast } from "../toast/ToastContext";
import { selectError } from "../../store/reducers/error/error.slice";
import ProtectedRoute from "../routes/ProtectedRoute";
const JobsPage = Loadable({
loader: () => import("../../screens/jobs/JobsPage"),
loading: () => <PageLoader loadingText="Getting your jobs..." />
});
const ProtectedLayout = () => {
const { openToast } = useToast();
const { error } = useSelector(selectError);
const getErrorDetails = async () => {
if (error) {
if (error?.title || error?.message)
return { title: error?.title, message: error?.message };
return {
title: "Error",
message: `Something went wrong. We couldn't complete this request`
};
}
return null;
};
useEffect(() => {
let isMounted = true;
getErrorDetails().then(
(e) =>
isMounted &&
(e?.title || e?.message) &&
openToast({ type: "error", title: e?.title, message: e?.message })
);
return () => {
isMounted = false;
};
}, [error]);
return (
<Switch>
<ProtectedRoute exact path="/" component={JobsPage} />
</Switch>
);
};
export default ProtectedLayout;
ProtectLayout returns another component ProtectedRoute. ProtectedRoute renders a react-router-dom Route component, which the component prop on the Route in the component prop passed into ProtectedRoute but wrapped in a Higher Order Component. In my actual application, as aforementioned, this is the withAuthenticationRequired HOC from #auth0/auth0-react which checks if an auth0 user is logged in, otherwise it logs the user out and redirects to the correct URL.
import React from "react";
import { Route } from "react-router-dom";
const withAuthenticationRequired = (Component, options) => {
return function WithAuthenticationRequired(props) {
return <Component {...props} />;
};
};
const ProtectedRoute = ({ component, ...args }) => {
return <Route component={withAuthenticationRequired(component)} {...args} />;
};
export default ProtectedRoute;
However, in one of the Route components, JobsPage the error reducer state is updated on mount, so what happens is the state gets updated, the ProtectedLayout re-renders, which then re-renders ProtectedRoute, which then re-renders JobPage which triggers the useEffect again, which updates the state, so you end up in an infinite loop.
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getGlobalError } from "../../store/reducers/error/error.thunk";
const JobsPage = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getGlobalError(new Error("test")));
}, []);
return (
<div>
JOBS PAGE
</div>
);
};
export default JobsPage;
I have no idea how to prevent this rendering loop?
Really all I want to do, is that when there is an error thrown in a thunk action, it catches the error and updates the error reducer state. That will then trigger a toast message, using the useToast hook. Perhaps there is a better way around this, that what I currently have setup?
I have a CodeSandbox below to recreate this issue. If you click on the text you can see the re-renders occur, if you comment out the useEffect hook, it will basically crash the sandbox, so might be best to only uncomment when you think you have resolved the issue.
Any help would be greatly appreciated!
Just to let you know, I don't yet know how to use class based components, setState, etc. I also don't know how to use other things in async js like axios or whatever else yet. This is what I can do below. Very basic.
This is App.js:
import Questions from './components/Questions.js'
import './index.css'
import {React, useState, useEffect} from 'react'
function App() {
const [questions, setQuestions] = useState([])
useEffect(() => {
async function getQuestions(){
const response = await fetch("https://opentdb.com/api.php?amount=5")
const data = await response.json()
setQuestions(() => data.results)
}
getQuestions()
}, [])
const questionBank = questions.map(singleQuestion => {
<Questions
question={singleQuestion.question}
/>
})
console.log(questions[0].question)
return (
<div>
{questionBank}
</div>
);
}
export default App;
For some reason, console.log(questions[0].question) when typed in to the editor and saved for the first time, it shows a question from the api. But after refreshing the page it doesn't show a question but it says: App.js:44 Uncaught TypeError: Cannot read properties of undefined (reading 'question') But when I just do this: console.log(questions[0]), it shows the first object of the array from the API no problem. I'm confused.
Also, questionBank doesn't render at all for some reason.
This is Questions.js:
import React from 'react'
export default function Questions(props){
return(
<div>
<p>{props.question}</p>
<br />
</div>
)
}
This is index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
<App />
)
There are few things to improve in your code (such as maybe make a useCallback out of function getQuestions in the useEffect), but the biggest issue is that you are not properly returning JSX from the map method.
Your code:
const questionBank = questions.map(singleQuestion => {
<Questions
question={singleQuestion.question}
/>
})
notice the curly braces { & }. The proper code:
const questionBank = questions.map(singleQuestion => (
<Questions
question={singleQuestion.question}
/>
))
After this change, your code should work properly.
Also, your console.log will cause errors, because you are console.logging before even fetching theses questions, so obviously questions[0] is undefined.
More improvements to the code:
export default function App() {
const [questions, setQuestions] = useState([]);
const getQuestions = useCallback(async () => {
const response = await fetch("https://opentdb.com/api.php?amount=5");
const data = await response.json();
setQuestions(data.results);
}, []);
useEffect(() => {
getQuestions();
}, [getQuestions]);
const questionBank = questions.map((singleQuestion) => (
<Questions question={singleQuestion.question} />
));
return <div>{questionBank}</div>;
}
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.
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