I am getting this warning in react:
index.js:1 Warning: Cannot update a component (`ConnectFunction`)
while rendering a different component (`Register`). To locate the
bad setState() call inside `Register`
I went to the locations indicated in the stack trace and removed all setstates but the warning still persists. Is it possible this could occur from redux dispatch?
my code:
register.js
class Register extends Component {
render() {
if( this.props.registerStatus === SUCCESS) {
// Reset register status to allow return to register page
this.props.dispatch( resetRegisterStatus()) # THIS IS THE LINE THAT CAUSES THE ERROR ACCORDING TO THE STACK TRACE
return <Redirect push to = {HOME}/>
}
return (
<div style = {{paddingTop: "180px", background: 'radial-gradient(circle, rgba(106,103,103,1) 0%, rgba(36,36,36,1) 100%)', height: "100vh"}}>
<RegistrationForm/>
</div>
);
}
}
function mapStateToProps( state ) {
return {
registerStatus: state.userReducer.registerStatus
}
}
export default connect ( mapStateToProps ) ( Register );
function which triggers the warning in my registerForm component called by register.js
handleSubmit = async () => {
if( this.isValidForm() ) {
const details = {
"username": this.state.username,
"password": this.state.password,
"email": this.state.email,
"clearance": this.state.clearance
}
await this.props.dispatch( register(details) )
if( this.props.registerStatus !== SUCCESS && this.mounted ) {
this.setState( {errorMsg: this.props.registerError})
this.handleShowError()
}
}
else {
if( this.mounted ) {
this.setState( {errorMsg: "Error - registration credentials are invalid!"} )
this.handleShowError()
}
}
}
Stacktrace:
This warning was introduced since React V16.3.0.
If you are using functional components you could wrap the setState call into useEffect.
Code that does not work:
const HomePage = (props) => {
props.setAuthenticated(true);
const handleChange = (e) => {
props.setSearchTerm(e.target.value.toLowerCase());
};
return (
<div key={props.restInfo.storeId} className="container-fluid">
<ProductList searchResults={props.searchResults} />
</div>
);
};
Now you can change it to:
const HomePage = (props) => {
// trigger on component mount
useEffect(() => {
props.setAuthenticated(true);
}, []);
const handleChange = (e) => {
props.setSearchTerm(e.target.value.toLowerCase());
};
return (
<div key={props.restInfo.storeId} className="container-fluid">
<ProductList searchResults={props.searchResults} />
</div>
);
};
I just had this issue and it took me a bit of digging around before I realised what I'd done wrong – I just wasn't paying attention to how I was writing my functional component.
I was doing this:
const LiveMatches = (props: LiveMatchesProps) => {
const {
dateMatches,
draftingConfig,
sportId,
getDateMatches,
} = props;
if (!dateMatches) {
const date = new Date();
getDateMatches({ sportId, date });
};
return (<div>{component stuff here..}</div>);
};
I had just forgotten to use useEffect before dispatching my redux call of getDateMatches()
So it should have been:
const LiveMatches = (props: LiveMatchesProps) => {
const {
dateMatches,
draftingConfig,
sportId,
getDateMatches,
} = props;
useEffect(() => {
if (!dateMatches) {
const date = new Date();
getDateMatches({ sportId, date });
}
}, [dateMatches, getDateMatches, sportId]);
return (<div>{component stuff here..}</div>);
};
please read the error message thoroughly, mine was pointing to SignIn Component that had a bad setState. which when i examined, I had an onpress that was not an Arrow function.
it was like this:
onPress={navigation.navigate("Home", { screen: "HomeScreen" })}
I changed it to this:
onPress={() => navigation.navigate("Home", { screen: "HomeScreen" }) }
My error message was:
Warning: Cannot update a component
(ForwardRef(BaseNavigationContainer)) while rendering a different
component (SignIn). To locate the bad setState() call inside
SignIn, follow the stack trace as described in
https://reactjs.org/link/setstate-in-render
in SignIn (at SignInScreen.tsx:20)
I fixed this issue by removing the dispatch from the register components render method to the componentwillunmount method. This is because I wanted this logic to occur right before redirecting to the login page. In general it's best practice to put all your logic outside the render method so my code was just poorly written before. Hope this helps anyone else in future :)
My refactored register component:
class Register extends Component {
componentWillUnmount() {
// Reset register status to allow return to register page
if ( this.props.registerStatus !== "" ) this.props.dispatch( resetRegisterStatus() )
}
render() {
if( this.props.registerStatus === SUCCESS ) {
return <Redirect push to = {LOGIN}/>
}
return (
<div style = {{paddingTop: "180px", background: 'radial-gradient(circle, rgba(106,103,103,1) 0%, rgba(36,36,36,1) 100%)', height: "100vh"}}>
<RegistrationForm/>
</div>
);
}
}
I think that this is important.
It's from this post that #Red-Baron pointed out:
#machineghost : I think you're misunderstanding what the message is warning about.
There's nothing wrong with passing callbacks to children that update state in parents. That's always been fine.
The problem is when one component queues an update in another component, while the first component is rendering.
In other words, don't do this:
function SomeChildComponent(props) {
props.updateSomething();
return <div />
}
But this is fine:
function SomeChildComponent(props) {
// or make a callback click handler and call it in there
return <button onClick={props.updateSomething}>Click Me</button>
}
And, as Dan has pointed out various times, queuing an update in the same component while rendering is fine too:
function SomeChildComponent(props) {
const [number, setNumber] = useState(0);
if(props.someValue > 10 && number < 5) {
// queue an update while rendering, equivalent to getDerivedStateFromProps
setNumber(42);
}
return <div>{number}</div>
}
If useEffect cannot be used in your case or if the error is NOT because of Redux
I used setTimeout to redirect one of the two useState variables to the callback queue.
I have one parent and one child component with useState variable in each of them. The solution is to wrap useState variable using setTimeout:
setTimeout(() => SetFilterData(data), 0);
Example below
Parent Component
import ExpenseFilter from '../ExpensesFilter'
function ExpensesView(props) {
const [filterData, SetFilterData] = useState('')
const GetFilterData = (data) => {
// SetFilterData(data);
//*****WRAP useState VARIABLE INSIDE setTimeout WITH 0 TIME AS BELOW.*****
setTimeout(() => SetFilterData(data), 0);
}
const filteredArray = props.expense.filter(expenseFiltered =>
expenseFiltered.dateSpent.getFullYear().toString() === filterData);
return (
<Window>
<div>
<ExpenseFilter FilterYear = {GetFilterData}></ExpenseFilter>
Child Component
const ExpensesFilter = (props) => {
const [filterYear, SetFilterYear] = useState('2022')
const FilterYearListener = (event) => {
event.preventDefault()
SetFilterYear(event.target.value)
}
props.FilterYear(filterYear)
return (
Using React and Material UI (MUI)
I changed my code from:
<IconButton onClick={setOpenDeleteDialog(false)}>
<Close />
</IconButton>
To:
<IconButton onClick={() => setOpenDeleteDialog(false)}>
<Close />
</IconButton>
Simple fix
If you use React Navigation and you are using the setParams or setOptions you must put these inside method componentDidMount() of class components or in useEffects() hook of functional components.
Minimal reproducing example
I was a bit confused as to what exactly triggers the problem, having a minimal immediately runnable example helped me grasp it a little better:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/#babel/standalone#7.14.7/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function NotMain(props) {
props.setN(1)
return <div>NotMain</div>
}
function Main(props) {
const [n, setN] = React.useState(0)
return <>
<NotMain setN={setN} />
<div>Main {n}</div>
</>
}
ReactDOM.render(
<Main/>,
document.getElementById('root')
);
</script>
</body>
</html>
fails with error:
react-dom.development.js:61 Warning: Cannot update a component (`Main`) while rendering a different component (`NotMain`). To locate the bad setState() call inside `NotMain`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
followed by a stack trace:
at NotMain (<anonymous>:16:9)
at Main (<anonymous>:21:31)
Presumably 16:9 would be the exact line where props.setN(1) is being called from, but the line numbers are a bit messed up because of the Babel JSX translation.
The solution like many other answers said is to do instead:
function NotMain(props) {
React.useEffect(() => { props.setN(1) }, [])
return <div>NotMain</div>
}
Intuitively, I think that the general idea of why this error happens is that:
You are not supposed to updat state from render methods, otherwise it could lead to different results depending on internal the ordering of how React renders things.
and when using functional components, the way to do that is to use hooks. In our case, useEffect will run after rendering is done, so we are fine doing that from there.
When using classes this becomes slightly more clear and had been asked for example at:
Calling setState in render is not avoidable
Calling setState() in React from render method
When using functional components however, things are conceptually a bit more mixed, as the component function is both the render, and the code that sets up the callbacks.
I was facing same issue, The fix worked for me was if u are doing
setParams/setOptions
outside of useEffect then this issue is occurring. So try to do such things inside useEffect. It'll work like charm
TL;DR;
For my case, what I did to fix the warning was to change from useState to useRef
react_devtools_backend.js:2574 Warning: Cannot update a component (`Index`) while rendering a different component (`Router.Consumer`). To locate the bad setState() call inside `Router.Consumer`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
at Route (http://localhost:3000/main.bundle.js:126692:29)
at Index (http://localhost:3000/main.bundle.js:144246:25)
at Switch (http://localhost:3000/main.bundle.js:126894:29)
at Suspense
at App
at AuthProvider (http://localhost:3000/main.bundle.js:144525:23)
at ErrorBoundary (http://localhost:3000/main.bundle.js:21030:87)
at Router (http://localhost:3000/main.bundle.js:126327:30)
at BrowserRouter (http://localhost:3000/main.bundle.js:125948:35)
at QueryClientProvider (http://localhost:3000/main.bundle.js:124450:21)
The full code for the context of what I did (changed from the lines with // OLD: to the line above them). However this doesn't matter, just try changing from useState to useRef!!
import { HOME_PATH, LOGIN_PATH } from '#/constants';
import { NotFoundComponent } from '#/routes';
import React from 'react';
import { Redirect, Route, RouteProps } from 'react-router-dom';
import { useAccess } from '#/access';
import { useAuthContext } from '#/contexts/AuthContext';
import { AccessLevel } from '#/models';
type Props = RouteProps & {
component: Exclude<RouteProps['component'], undefined>;
requireAccess: AccessLevel | undefined;
};
export const Index: React.FC<Props> = (props) => {
const { component: Component, requireAccess, ...rest } = props;
const { isLoading, isAuth } = useAuthContext();
const access = useAccess();
const mounted = React.useRef(false);
// OLD: const [mounted, setMounted] = React.useState(false);
return (
<Route
{...rest}
render={(props) => {
// If in indentifying authentication state as the page initially loads, render a blank page
if (!mounted.current && isLoading) return null;
// OLD: if (!mounted && isLoading) return null;
// 1. Check Authentication is one step
if (!isAuth && window.location.pathname !== LOGIN_PATH)
return <Redirect to={LOGIN_PATH} />;
if (isAuth && window.location.pathname === LOGIN_PATH)
return <Redirect to={HOME_PATH} />;
// 2. Authorization is another
if (requireAccess && !access[requireAccess])
return <NotFoundComponent />;
mounted.current = true;
// OLD: setMounted(true);
return <Component {...props} />;
}}
/>
);
};
export default Index;
My example.
Code with that error:
<Form
initialValues={{ ...kgFormValues, dataflow: dataflows.length > 0 ? dataflows[0].df_tpl_key : "" }}
onSubmit={() => {}}
render={({values, dirtyFields }: any) => {
const kgFormValuesUpdated = {
proj_key: projectKey,
name: values.name,
description: values.description,
public: values.public,
dataflow: values.dataflow,
flavours: flavoursSelected,
skipOCR: values.skipOCR
};
if (!_.isEqual(kgFormValues, kgFormValuesUpdated)) {
setNewKgFormValues(kgFormValuesUpdated);
}
Working Code:
<Form
initialValues={{ ...kgFormValues, dataflow: dataflows.length > 0 ? dataflows[0].df_tpl_key : "" }}
onSubmit={() => {}}
render={({ values, dirtyFields }: any) => {
useEffect(() => {
const kgFormValuesUpdated = {
proj_key: projectKey,
name: values.name,
description: values.description,
public: values.public,
dataflow: values.dataflow,
flavours: flavoursSelected,
skipOCR: values.skipOCR
};
if (!_.isEqual(kgFormValues, kgFormValuesUpdated)) {
setNewKgFormValues(kgFormValuesUpdated);
}
}, [values]);
return (
I had the same problem. I was setting some state that was storing a function like so:
// my state definition
const [onConfirm, setOnConfirm] = useState<() => void>();
// then I used this piece of code to update the state
function show(onConfirm: () => void) {
setOnConfirm(onConfirm);
}
The problem was from setOnConfirm. In React, setState can take the new value OR a function that returns the new value. In this case React wanted to get the new state from calling onConfirm which is not correct.
changing to this resolved my issue:
setOnConfirm(() => onConfirm);
I was able to solve this after coming across a similar question in GitHub which led me to this comment showing how to pinpoint the exact line within your file causing the error. I wasn't aware that the stack trace was there. Hopefully this helps someone!
See below for my fix. I simply converted the function to use callback.
Old code
function TopMenuItems() {
const dispatch = useDispatch();
function mountProjectListToReduxStore(projects) {
const projectDropdown = projects.map((project) => ({
id: project.id,
name: project.name,
organizationId: project.organizationId,
createdOn: project.createdOn,
lastModifiedOn: project.lastModifiedOn,
isComplete: project.isComplete,
}));
projectDropdown.sort((a, b) => a.name.localeCompare(b.name));
dispatch(loadProjectsList(projectDropdown));
dispatch(setCurrentOrganizationId(projectDropdown[0].organizationId));
}
};
New code
function TopMenuItems() {
const dispatch = useDispatch();
const mountProjectListToReduxStore = useCallback((projects) => {
const projectDropdown = projects.map((project) => ({
id: project.id,
name: project.name,
organizationId: project.organizationId,
createdOn: project.createdOn,
lastModifiedOn: project.lastModifiedOn,
isComplete: project.isComplete,
}));
projectDropdown.sort((a, b) => a.name.localeCompare(b.name));
dispatch(loadProjectsList(projectDropdown));
dispatch(setCurrentOrganizationId(projectDropdown[0].organizationId));
}, [dispatch]);
};
My case was using setState callback, instead of setState + useEffect
BAD ❌
const closePopover = useCallback(
() =>
setOpen((prevOpen) => {
prevOpen && onOpenChange(false);
return false;
}),
[onOpenChange]
);
GOOD ✅
const closePopover = useCallback(() => setOpen(false), []);
useEffect(() => onOpenChange(isOpen), [isOpen, onOpenChange]);
I got this when I was foolishly invoking a function that called dispatch instead of passing a reference to it for onClick on a button.
const quantityChangeHandler = (direction) => {
dispatch(cartActions.changeItemQuantity({title, quantityChange: direction}));
}
...
<button onClick={() => quantityChangeHandler(-1)}>-</button>
<button onClick={() => quantityChangeHandler(1)}>+</button>
Initially, I was directly calling without the fat arrow wrapper.
Using some of the answers above, i got rid of the error with the following:
from
if (value === "newest") {
dispatch(sortArticlesNewest());
} else {
dispatch(sortArticlesOldest());
}
this code was on my component top-level
to
const SelectSorting = () => {
const dispatch = useAppDispatch();
const {value, onChange} = useSelect();
useEffect(() => {
if (value === "newest") {
dispatch(sortArticlesNewest());
} else {
dispatch(sortArticlesOldest());
}
}, [dispatch, value]);
In my React app I have a page which has a list of web items (WebItems.js) and on the page there is an Link which goes to a page for adding a new web item (Add.js).
const WebItems = () => (
async function getWebItems() {
const result = await httpGet('webitem/list');
if (result.items) {
setWebItems(result.items);
}
}
return <>
<Link
to="webitem/list"
Add
</Link>
</>
)
I need to pass the function getWebItems() to the Add component so that after a web item is added the list of web items gets updated.
I am more familiar with #reach/router although I need to use react-router-dom for this project.
I found this blog post but I am unsure if this is what I need.
Can anyone help?
Why don't you just make this function a custom hook and re-use it?
// useWebItems.js
import { useState } from 'react';
function useWebItems() {
const [webItems, setWebItems] = useState([]);
async function getWebItems() {
const result = await httpGet('webitem/list');
if (result.items) {
setWebItems(result.items);
}
}
return { getWebItems, webItems };
}
export default useWebItems;
// Add Component
import { getWebItems, webItems } from 'path/to/useWebItems.js';
// Do whatever you want with getWebItems, webItems
You could use route state and the object form of the to prop for the Link.
const WebItems = () => (
async function getWebItems() {
const result = await httpGet('webitem/list');
if (result.items) {
setWebItems(result.items);
}
}
return <>
<Link
to={{
pathname: "webitem/list",
state: {
getWebItems,
},
}}
>
Add
</Link>
</>
);
Use location from the route props on the receiving route/component
const { getWebItems } = props.location.state;
...
getWebItems();
I am trying to figure out how to define a link to reference that can use a firebase document id to link to a show view for that document. I can render an index. I cannot find a way to define a link to the document.
I've followed this tutorial - which is good to get the CRUD steps other than the show view. I can find other tutorials that do this with class components and the closest I've been able to find using hooks is this incomplete project repo.
I want to try and add a link in the index to show the document in a new view.
I have an index with:
const useBlogs = () => {
const [blogs, setBlogs] = useState([]); //useState() hook, sets initial state to an empty array
useEffect(() => {
const unsubscribe = Firebase
.firestore //access firestore
.collection("blog") //access "blogs" collection
.where("status", "==", true)
.orderBy("createdAt")
.get()
.then(function(querySnapshot) {
// .onSnapshot(snapshot => {
//You can "listen" to a document with the onSnapshot() method.
const listBlogs = querySnapshot.docs.map(doc => ({
//map each document into snapshot
id: doc.id, //id and data pushed into blogs array
...doc.data() //spread operator merges data to id.
}));
setBlogs(listBlogs); //blogs is equal to listBlogs
});
return
// () => unsubscribe();
}, []);
return blogs;
};
const BlogList = ({ editBlog }) => {
const listBlog = useBlogs();
return (
<div>
{listBlog.map(blog => (
<Card key={blog.id} hoverable={true} style={{marginTop: "20px", marginBottom: "20px"}}>
<Title level={4} >{blog.title} </Title>
<Tag color="geekblue" style={{ float: "right"}}>{blog.category} </Tag>
<Paragraph><Text>{blog.caption}
</Text></Paragraph>
<Link to={`/readblog/${blog.id}`}>Read</Link>
<Link to={`/blog/${blog.id}`}>Read</Link>
</Card>
))}
</div>
);
};
export default BlogList;
Then I have a route defined with:
export const BLOGINDEX = '/blog';
export const BLOGPOST = '/blog/:id';
export const NEWBLOG = '/newblog';
export const EDITBLOG = '/editblog';
export const VIEWBLOG = '/viewblog';
export const READBLOG = '/readblog/:id';
I can't find a tutorial that does this with hooks. Can anyone see how to link from an index to a document that I can show in a different page?
I did find this code sandbox. It looks like it is rendering a clean page in the updateCustomer page and using data from the index to do it - but the example is too clever for me to unpick without an explanation of what's happening (in particular, the updateCustomer file defines a setCustomer variable, by reference to useForm - but there is nothing in useForm with that definition. That variable is used in the key part of the file that tries to identify the data) - so I can't mimic the steps.
NEXT ATTEMPT
I found this blog post which suggests some changes for locating the relevant document.
I implemented these changes and while I can print the correct document.id on the read page, I cannot find a way to access the document properties (eg: blog.title).
import React, { useHook } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
function ReadBlogPost() {
let { slug } = useParams()
// ...
return (
<div>{slug}
</div>
)
};
export default ReadBlogPost;
NEXT ATTEMPT:
I tried to use the slug as the doc.id to get the post document as follows:
import React, { useHook, useEffect } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
function ReadBlogPost() {
let { slug } = useParams()
// ...
useEffect(() => {
const blog =
Firebase.firestore.collection("blog").doc(slug);
blog.get().then(function(doc) {
if (doc.exists) {
console.log("Document data:", doc.data());
doc.data();
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
});
return (
<div>{blog.title}
</div>
)
};
export default ReadBlogPost;
It returns an error saying blog is not defined. I also tried to return {doc.title} but I get the same error. I can see all the data in the console.
I really can't make sense of coding documentation - I can't figure out the starting point to decipher the instructions so most things I learn are by trial and error but I've run out of places to look for inspiration to try something new.
NEXT ATTEMPT
My next attempt is to try and follow the lead in this tutorial.
function ReadBlogPost(blog) {
let { slug } = useParams()
// ...
useEffect(() => {
const blog =
Firebase.firestore.collection("blog").doc(slug);
blog.get().then(function(doc) {
if (doc.exists) {
doc.data()
console.log("Document data:", doc.data());
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
},
[blog]
);
return (
<div><Title level={4} > {blog.title}
</Title>
<p>{console.log(blog)}</p>
</div>
)
};
export default ReadBlogPost;
When I try this, the only odd thing is that the console.log inside the useEffect method gives all the data accurately, but when I log it form inside the return method, I get a load of gibberish (shown in the picture below).
NEXT ATTEMPT
I found this tutorial, which uses realtime database instead of firestore, but I tried to copy the logic.
My read post page now has:
import React, { useHook, useEffect, useState } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
import { Card, Divider, Form, Icon, Input, Switch, Layout, Tabs, Typography, Tag, Button } from 'antd';
const { Paragraph, Text, Title } = Typography;
const ReadBlogPost = () => {
const [loading, setLoading] = useState(true);
const [currentPost, setCurrentPost] = useState();
let { slug } = useParams()
if (loading && !currentPost) {
Firebase
.firestore
.collection("blog")
.doc(slug)
.get()
.then(function(doc) {
if (doc.exists) {
setCurrentPost(...doc.data());
console.log("Document data:", doc.data());
}
}),
setLoading(false)
}
if (loading) {
return <h1>Loading...</h1>;
}
return (
<div><Title level={4} >
{currentPost.caption}
{console.log({currentPost})}
</Title>
</div>
)
};
export default ReadBlogPost;
Maybe this blog post is old, or maybe it's to do with it using .js where I have .jsx - which I think means I can't use if statements, but I can't get this to work either. The error says:
Line 21:9: Expected an assignment or function call and instead saw
an expression no-unused-expressions
It points to the line starting with Firebase.
I got rid of all the loading bits to try and make the data render. That gets rid of the above error message for now. However, I still can't return the values from currentPost.
It's really odd to me that inside the return statement, I cannot output {currentPost.title} - I get an error saying title is undefined, but when I try to output {currentPost} the error message says:
Error: Objects are not valid as a React child (found: object with keys
{caption, category, createdAt, post, status, title}). If you meant to
render a collection of children, use an array instead.
That makes no sense! I'd love to understand why I can log these values before the return statement, and inside the return statement, I can log them on the object but I cannot find how to log them as attributes.
First of all: is your useBlog() hook returning the expected data? If so, all you need to do is define your <Link/> components correctly.
<Link
// This will look like /readblog/3. Curly braces mean
// that this prop contains javascript that needs to be
// evaluated, thus allowing you to create dynamic urls.
to={`/readblog/${blog.id}`}
// Make sure to open in a new window
target="_blank"
>
Read
</Link>
Edit: If you want to pass the data to the new component you need to set up a store in order to avoid fetching the same resource twice (once when mounting the list and once when mounting the BlogPost itself)
// Define a context
const BlogListContext = React.createContext()
// In a top level component (eg. App.js) define a provider
const App = () => {
const [blogList, setBlogList] = useState([])
return (
<BlogListContext.Provider value={{blogList, setBlogList}}>
<SomeOtherComponent/>
</BlogListContext.Provider>
)
}
// In your BlogList component
const BlogList = ({ editBlog }) => {
const { setBlogList } = useContext(BlogListContext)
const listBlog = useBlogs()
// Update the blog list from the context each time the
// listBlog changes
useEffect(() => {
setBlogList(listBlog)
}, [listBlog])
return (
// your components and links here
)
}
// In your ReadBlog component
const ReadBlogComponent = ({ match }) => {
const { blogList } = useContext(BlogListContext)
// Find the blog by the id from params.
const blog = blogList.find(blog => blog.id === match.params.id) || {}
return (
// Your JSX
)
}
There are other options for passing data as well:
Through url params (not recommended).
Just pass the ID and let the component fetch its own data on mount.
I found an answer that works for each attribute other than the timestamp.
const [currentPost, setCurrentPost] = useState([]);
There is an empty array in the useState() initialised state.
In relation to the timestamps - I've been through this hell so many times with firestore timestamps - most recently here. The solution that worked in December 2019 no longer works. Back to tearing my hair out over that one...
I have issues with communication between a parent and a child component.
I would like the parent (Host) to hold his own state. I would like the child (Guest) to be passed that state and modify it. The child has his local version of the state which can change however the child wants. However, once the child finishes playing with the state, he passes it up to the parent to actually "Save" the actual state.
How would I correctly implement this?
Issues from my code:
on the updateGlobalData handler, I log both data and newDataFromGuest and they are the same. I would like data to represent the old version of the data, and newDataFromGuest to represent the new
updateGlobalData is being called 2X. I can solve this by removing the updateGlobalData ref from the deps array inside useEffect but I don't want to heck it.
My desired results should be:
the data state should hold the old data until updateGlobalData is called
I want updateGlobalData to be fired only once when I click the button
Code from Codesandbox:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const Host = () => {
const [data, setData] = useState({ foo: { bar: 1 } });
const updateGlobalData = newDataFromGuest => {
console.log(data);
console.log(newDataFromGuest);
setData(newDataFromGuest);
};
return <Guest data={data} updateGlobalData={updateGlobalData} />;
};
const Guest = ({ data, updateGlobalData }) => {
const [localData, setLocalData] = useState(data);
const changeLocalData = newBarNumber => {
localData.foo = { bar: newBarNumber };
setLocalData({ ...localData });
};
useEffect(() => {
updateGlobalData(localData);
}, [localData, updateGlobalData]);
return (
<div>
<span>{localData.foo.bar}</span> <br />
<button onClick={() => changeLocalData(++localData.foo.bar)}>
Increment
</button>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Host />, rootElement);
NOTE: Code solution below
Problem 1:
I want updateGlobalData to be fired only once when I click the button
To solve this issue, I have used a mix between React.createContext and the hook useReducer. The idea is to make the Host dispatcher available through its context. This way, you do not need to send the "updateGlobalData" callback down to the Guest, nor make the useEffect hook to be dependant of it. Thus, useEffect will be triggered only once.
Note though, that useEffect now depends on the host dipatcher and you need to include it on its dependencies. Nevertheless, if you read the first note on useReducer, a dispatcher is stable and will not cause a re-render.
Problem 2:
the data state should hold the old data until updateGlobalData is called
The solution is easy: DO NOT CHANGE STATE DATA DIRECTLY!! Remember that most values in Javascript are passed by reference. If you send data to the Guest and you directly modify it, like here
const changeLocalData = newBarNumber => {
localData.foo = { bar: newBarNumber }; // YOU ARE MODIFYING STATE DIRECTLY!!!
...
};
and here
<button onClick={() => changeLocalData(++localData.foo.bar)}> // ++ OPERATOR MODIFYES STATE DIRECLTY
they will also be modified in the Host, unless you change that data through the useState hook. I think (not 100% sure) this is because localData in Guest is initialized with the same reference as data coming from Host. So, if you change it DIRECTLY in Guest, it will also be changed in Host. Just add 1 to the value of your local data in order to update the Guest state, without using the ++ operator. Like this:
localData.foo.bar + 1
This is my solution:
import React, { useState, useEffect, useReducer, useContext } from "react";
import ReactDOM from "react-dom";
const HostContext = React.createContext(null);
function hostReducer(state, action) {
switch (action.type) {
case "setState":
console.log("previous Host data value", state);
console.log("new Host data value", action.payload);
return action.payload;
default:
throw new Error();
}
}
const Host = () => {
// const [data, setData] = useState({ foo: { bar: 1 } });
// Note: `dispatch` won't change between re-renders
const [data, dispatch] = useReducer(hostReducer, { foo: { bar: 1 } });
// const updateGlobalData = newDataFromGuest => {
// console.log(data.foo.bar);
// console.log(newDataFromGuest.foo.bar);
// setData(newDataFromGuest);
// };
return (
<HostContext.Provider value={dispatch}>
<Guest data={data} /*updateGlobalData={updateGlobalData}*/ />
</HostContext.Provider>
);
};
const Guest = ({ data /*, updateGlobalData*/ }) => {
// If we want to perform an action, we can get dispatch from context.
const hostDispatch = useContext(HostContext);
const [localData, setLocalData] = useState(data);
const changeLocalData = newBarNumber => {
// localData.foo = { bar: newBarNumber };
// setLocalData({ ...localData });
setLocalData({ foo: { bar: newBarNumber } });
};
useEffect(() => {
console.log("useEffect", localData);
hostDispatch({ type: "setState", payload: localData });
// updateGlobalData(localData);
}, [localData, hostDispatch /*, updateGlobalData*/]);
return (
<div>
<span>{localData.foo.bar}</span> <br />
<button onClick={() => changeLocalData(localData.foo.bar + 1)}>
Increment
</button>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Host />, rootElement);
If you see anything not matching with what you want, please, let me know and I will re-check it.
I hope it helps.
Best,
Max.
I have a React.SFC / react stateless / functional component which is unfortunately rendering a little too frequent due to some excess data coming in from redux in a parent component. Nothing I can do about that for now, so I'm just accepting the extra rerenders, and using useEffect to make sure data is only fetched whenever a certain property changes. In this case its called "urls" and it is an array of URL's (TypeScript URL Type).
Here's some example code illustrating the issue:
import React from "react";
import { useState, useEffect, useMemo } from "react";
import { render } from "react-dom";
import "./styles.css";
const useCustomHook = urls => {
const [onlyChangeWhenUrlsChange, setOnlyChangeWhenUrlsChange] = useState(
null
);
useEffect(
() => {
setOnlyChangeWhenUrlsChange(Math.random());
},
[urls]
);
return onlyChangeWhenUrlsChange;
};
const dynamicUrls = (pageRouteParamId, someDynamicUrlParam) => {
return [
{
pageRouteParamId: 1337,
urls: [new URL(`https://someurl.com/api?id=${someDynamicUrlParam}`)]
}
];
};
const SomePage: React.SFC<any> = ({
simulateFrequentUpdatingData,
pageRouteParamId
}) => {
const someOtherId = 1;
// As suggested in SO answer, using useMemo seems to work, but will that not create a memory leak?
// Is there any good alternative?
// const urls = useMemo(() => dynamicUrls(pageRouteParamId, someOtherId).find(url => url.pageRouteParamId === pageRouteParamId).urls, [pageRouteParamId, someOtherId]);
const urls = dynamicUrls(pageRouteParamId, 1).find(
url => url.pageRouteParamId === 1337
).urls;
return (
<div>
<p>parent</p>
<p>{simulateFrequentUpdatingData}</p>
<p>
Page route param id (in real app this would come from react-router route
param): {pageRouteParamId}
</p>
{urls && urls.length && <MyStateLessFunctionalComponent {...{ urls }} />}
<p>
Page route param id (in real app this would come from react-router route
param): {pageRouteParamId}
</p>
{urls && urls.length && (
<MyStateLessFunctionalComponentWithHook {...{ urls }} />
)}
</div>
);
};
const MyStateLessFunctionalComponent: React.SFC<any> = ({ urls }) => {
const [onlyChangeWhenUrlsChange, setOnlyChangeWhenUrlsChange] = useState(
null
);
useEffect(
() => {
setOnlyChangeWhenUrlsChange(Math.random());
},
[urls]
);
return (
<div>
<p>MyStateLessFunctionalComponent</p>
<p>{JSON.stringify(urls)}</p>
<p>This should only change when urls change {onlyChangeWhenUrlsChange}</p>
</div>
);
};
const MyStateLessFunctionalComponentWithHook: React.SFC<any> = ({ urls }) => {
const onlyChangeWhenUrlsChange = useCustomHook(urls);
return (
<div>
<p>MyStateLessFunctionalComponentWithHook</p>
<p>{JSON.stringify(urls)}</p>
<p>This should only change when urls change {onlyChangeWhenUrlsChange}</p>
</div>
);
};
function App() {
const [
simulateFrequentUpdatingData,
setSimulateFrequentUpdatingData
] = useState(null);
const [pageRouteParamId, setPageRouteParamId] = useState(1337);
useEffect(() => {
setInterval(() => setSimulateFrequentUpdatingData(Math.random()), 1000);
}, []);
return (
<div className="App">
<SomePage {...{ simulateFrequentUpdatingData, pageRouteParamId }} />
</div>
);
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);
Edit:
I had to change the title and question, since while reproducing it with the example code I realized the problem was not about "Skipping Effects inside a custom hook". Before I though I saw a difference when using useEffect directly vs inside a custom hook, and as the comments rightfully mentioned, there should not be any difference - and I came to the same conclusion while reproducing my issue with this sample code:
You can check out a live example here.
As it was suggested in the answer below, it seems like useMemo solves the issue (see line 36)
My guess is that urls is being declared inside a render higher up the tree, and thus getting a new identity every time. You can either useMemo on the place where it is being declared, JSON.stringify the urls in the deps-array, or a useRef which works as an additional guard against re-runs.
Edit: This is being discussed by smarter people than me: https://github.com/facebook/react/issues/14476#issuecomment-471199055.
If urls is an array of strings you can pass that as the second argument to useEffect
useEffect(() => {
loadData();
}, urls);
that way it will check the string values instead of the array reference.