Prefacing this with a thought; I think I might require a recursive component but that's beyond my current ability with native js and React so I feel like I have Swiss cheese understanding of React at this point.
The problem:
I have an array of metafields containing metafield objects with the following structure:
{
metafields: [
{ 0:
{ namespace: "namespaceVal",
key: "keyVal",
val: [
0: "val1",
1: "val2",
2: "val3"
]
}
},
...
]
}
My code maps metafields into Cards and within each card lives a component <MetafieldInput metafields={metafields['value']} /> and within that component the value array gets mapped to input fields. Overall it looks like:
// App
render() {
const metafields = this.state.metafields;
return (
{metafields.map(metafield) => (
<MetafieldInputs metafields={metafield['value']} />
)}
)
}
//MetafieldInputs
this.state = { metafields: this.props.metafields}
render() {
const metafields = this.state;
return (
{metafields.map((meta, i) => (
<TextField
value={meta}
changeKey={meta}
onChange={(val) => {
this.setState(prevState => {
return { metafields: prevState.metafields.map((field, j) => {
if(j === i) { field = val; }
return field;
})};
});
}}
/>
))}
)
}
Up to this point everything displays correctly and I can change the inputs! However the change happens one at a time, as in I hit a key then I have to click back into the input to add another character. It seems like everything gets re-rendered which is why I have to click back into the input to make another change.
Am I able to use components in this way? It feels like I'm working my way into nesting components but everything I've read says not to nest components. Am I overcomplicating this issue? The only solution I have is to rip out the React portion and take it to pure javascript.
guidance would be much appreciated!
My suggestion is that to out source the onChange handler, and the code can be understood a little bit more easier.
Mainly React does not update state right after setState() is called, it does a batch job. Therefore it can happen that several setState calls are accessing one reference point. If you directly mutate the state, it can cause chaos as other state can use the updated state while doing the batch job.
Also, if you out source onChange handler in the App level, you can change MetafieldInputs into a functional component rather than a class-bases component. Functional based component costs less than class based component and can boost the performance.
Below are updated code, tested. I assume you use Material UI's TextField, but onChangeHandler should also work in your own component.
// Full App.js
import React, { Component } from 'react';
import MetafieldInputs from './MetafieldInputs';
class App extends Component {
state = {
metafields: [
{
metafield:
{
namespace: "namespaceVal",
key: "keyVal",
val: [
{ '0': "val1" },
{ '1': "val2" },
{ '2': "val3" }
]
}
},
]
}
// will never be triggered as from React point of view, the state never changes
componentDidUpdate() {
console.log('componentDidUpdate')
}
render() {
const metafields = this.state.metafields;
const metafieldsKeys = Object.keys(metafields);
const renderInputs = metafieldsKeys.map(key => {
const metafield = metafields[key];
return <MetafieldInputs metafields={metafield.metafield.val} key={metafield.metafield.key} />;
})
return (
<div>
{renderInputs}
</div>
)
}
}
export default App;
// full MetafieldInputs
import React, { Component } from 'react'
import TextField from '#material-ui/core/TextField';
class MetafieldInputs extends Component {
state = {
metafields: this.props.metafields
}
onChangeHandler = (e, index) => {
const value = e.target.value;
this.setState(prevState => {
const updateMetafields = [...prevState.metafields];
const updatedFields = { ...updateMetafields[index] }
updatedFields[index] = value
updateMetafields[index] = updatedFields;
return { metafields: updateMetafields }
})
}
render() {
const { metafields } = this.state;
// will always remain the same
console.log('this.props', this.props)
return (
<div>
{metafields.map((meta, i) => {
return (
<TextField
value={meta[i]}
changekey={meta}
onChange={(e) => this.onChangeHandler(e, i)}
// generally it is not a good idea to use index as a key.
key={i}
/>
)
}
)}
</div>
)
}
}
export default MetafieldInputs
Again, IF you out source the onChangeHandler to App class, MetafieldInputs can be a pure functional component, and all the state management can be done in the App class.
On the other hand, if you want to keep a pure and clean App class, you can also store metafields into MetafieldInputs class in case you might need some other logic in your application.
For instance, your application renders more components than the example does, and MetafieldInputs should not be rendered until something happened. If you fetch data from server end, it is better to fetch the data when it is needed rather than fetching all the data in the App component.
You need to do the onChange at the app level. You should just pass the onChange function into MetaFieldsInput and always use this.props.metafields when rendering
Related
I have a NavBar component that has a list of dynamically generated links (these links are generated after querying my backend for some categories). These links are stored inside a child component of the NavBar, called DrawerMenu.
The NavBar is a child of the main App.js component.
In my Category component, I have a "delete" function that deletes a category. Once I delete a category I want to remove the link to it in the NavBar. How would I go about doing this?
For further context, my components are given below:
DrawerMenu component
class DrawerMenu extends Component {
state = {
menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
}
getData = (query) => {
// Query backend for category data and set it to this.state.menuItems
}
componentDidMount() {
this.getData(menuItemsQuery)
}
render() {
const { classes, handleDrawerClose, open } = this.props
const { menuItems } = this.state
const drawer = (classes, handleDrawerClose) => (
<div>
...
{
menuItems.map((menuItem, index) => (
<Link color="inherit" key={index} to={menuItem.link} className={classes.drawerLink} component={RouterLink}>
<ListItem button className={classes.drawerListItem} onClick={handleDrawerClose}>
<ListItemText primary={menuItem.name} />
</ListItem>
</Link>
))
}
...
</div>
)
...
return (
<div>
<Drawer
variant="temporary"
anchor='left'
open={open}
onClose={handleDrawerClose}
classes={{
paper: `${open ? classes.drawerOpen : null} ${!open ? classes.drawerClose : null}`,
}}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
{drawer(classes, handleDrawerClose)}
</Drawer>
</div>
)
}
}
NavBar component
function PrimarySearchAppBar(props) {
return (
<div className={classes.grow}>
...
<DrawerMenu
classes={classes}
handleDrawerClose={handleDrawerClose}
open={open}
/>
...
</div>
)
}
Category component
class Category extends Component {
...
deleteCategory = async () => {
// Code to request backend to delete category
this.props.history.push(`/`)
}
...
}
There are two common ways of doing this: You can either use a state management tool, like Redux or pass your state down the component tree as props.
Redux is often used when several components depend on the same state or when the component that depends on a state is several layers deep, so it would get cumbersome to pass it down as props.
I'll assume your component tree is not very large, so I will create a simple example passing props down the tree.
class DrawerMenu extends Component {
// We're gonna manage the state here, so the deletion
// will actually be handled by this component
state = {
menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
}
handleDelete = (id) => {
let updatedMenuItem = [...this.state.menuItems]; //Create a copy
updatedMenuItem = updatedMenuItem(item => item.id !== id) // Remove the
deleted item
this.setState({
menuItems: updatedMenuItem
})
}
...
// Then wherever you render the category component
<Category handleDelete = {handleDelete}/> //Pass a reference to the delete method
}
Category Component
class Category extends Component {
...
deleteCategory = async () => {
// Code to request backend to delete category
this.props.handleDelete(categoryId) //Pass the id of the category
this.props.history.push(`/`)
}
...
}
I would suggest reading about state management, it is a core concept in React and you will use it everywhere. Redux and Context API for example.
Not sure why Dennis Vash deleted their answer, they are correct, but perhaps not descriptive enough in the solution.
The way you delete the category is not to call the backend itself from inside the category component, because then the navbar doesn't know that you made a call, but to call a callback that is in an ancestor shared by both the category component and the navbar to delete a category, and then rerequest the categories list from the server. In the example below, this ancestor that is shared is MyCategoriesProvider
Because the category component is likely to be in a much different place (or multiple places) in the tree than the NavBar, it's best to use context.
Honestly, this is a great place for redux, but I'm not going to push redux on you and instead will just demo a Context solution.
// We're going to create a context that will manage your categories
// The only job of this context is to hold the current categories,
// and supply the updating functions. For brevity, I'll just give
// it a handleDelete function.
// Ideally, you'd also store the status of the request in this context
// as well so you could show loaders in the app, etc
import { createContext } from 'react';
// export this, we'll be using it later
export const CategoriesContext = createContext();
// export this, we'll render it high up in the app
// it will only accept children
export const MyCategoriesProvider = ({children}) => {
// here we can add a status flag in case we wanted to show a spinner
// somewhere down in your app
const [isRequestingCategories,setIsRequestingCategories] = useState(false);
// this is your list of categories that you got from the server
// we'll start with an empty array
const [categories,setCategories] = useState([]);
const fetch = async () => {
setIsRequestingCategories(true);
setCategories(await apiCallToFetchCategories());
setIsRequestingCategories(false);
}
const handleDelete = async category => {
await apiCallToDeleteCategory(category);
// we deleted a category, so we should re-request the list from the server
fetch();
}
useEffect(() => {
// when this component mounts, fetch the categories immediately
fetch();
// feel free to ignore any warnings if you're using a linter about rules of hooks here - this is 100% a "componentDidMount" hook and doesn't have any dependencies
},[]);
return <CategoriesContext.Provider value={{categories,isRequestingCategories,handleDelete}}>{children}</CategoriesContext.Provider>
}
// And you use it like this:
const App = () => {
return (
<MyCategoriesProvider>
<SomeOtherComponent>
<SomeOtherComponent> <- let's say your PrimarySearchBar is in here somewhere
<SomeOtherComponent>
</MyCategoriesProvider>
)
}
// in PrimarySearchBar you'd do this:
function PrimarySearchBar(props) => {
const {categories} = useContext(CategoriesContext); // you exported this above, remember?
// pass it as a prop to navbar, you could easily put the useContext hook inside of any component
return <NavBar categories={categories}/>
}
// in your category component you could do this:
class Category extends Component {
render() {
// Don't forget, categoriesContext is the thing you exported way up at the top
<CategoriesContext.Consumer>
{({handleDelete}) => {
return <button onClick={() => handleDelete(this.props.category)}>
}}
</CategoriesContext.Consumer>
}
}
EDIT:
I see you're mixing class and functional components, which is fine. You should check out this article on how to use the context api in either of them - in functional components you typically use a useContext hook, while in class components you'll use a consumer.
I would just refresh the list of categories that come from the server, after the delete request is done.
I'd do it as follows:
I would make the drawer component not so smart, making it receive the list of menuItems.
<DrawerMenu
classes={classes}
handleDrawerClose={handleDrawerClose}
open={open}
items={/* ... */}
/>
This is an important step, because now, to refresh the list of items rendered, you just pass another list. The server-side logic remains disconnected from this component in this way.
I'm not sure where you render the Category components, but supposing it is rendered outside the PrimarySearchAppBar it seems that this menuItems might need to be passed to the components from an upper level. I see 2 solutions:
I'd do the request for the menuItems from the same place where I do the request for the categories:
const App = props => {
const [categories, setCategories] = React.useState([])
const [menuItems, setMenuItems] = React.useState([])
const fetchCategories = useCallback(()=> {
yourApi.getCategories().then(categories => setCategories(categories))
})
const fetchMenuItems = useCallback(() => {
yourApi.getMenuItems().then(menuItems => setMenuItems(menuItems))
})
useEffect(() => {
fetchCategories()
}, [])
useEffect(() => {
fetchMenuItems()
}, [categories])
const handleDeleteCategory = useCallback(idToDelete => {
yourApi.deleteCategory(idToDelete).then(fetchCategories)
})
return (
<div>
<PrimarySearchAppBar menuItems={menuItems}/>
<Categories categories={categories} onDeleteClick={handleDeleteCategory} />
</div>
)
}
you can do the same thing but do it with a provider and using the content API if you do not want to have all the logic here. It is good to have smart/fetches/server-side logic in a top level component and then pass down props to dumb components.
PS.
There is also a nice hook to make fetches easier:
https://github.com/doasync/use-promise
I currently use a custom version of a usePromise hook I found because I added some interesting features. I can share it if you want but I don't want to add noise to the answer.
I am using Redux with Class Components in React. Having the below two states in Redux store.
{ spinner: false, refresh: false }
In Parent Components, I have a dispatch function to change this states.
class App extends React.Component {
reloadHandler = () => {
console.log("[App] reloadComponent");
this.props.onShowSpinner();
this.props.onRefresh();
};
render() {
return <Child reloadApp={this.reloadHandler} />;
}
}
In Child Component, I am trying to reload the parent component like below.
class Child extends React.Component {
static getDerivedStateFromProps(props, state) {
if (somecondition) {
// doing some redux store update
props.reloadApp();
}
}
render() {
return <button />;
}
}
I am getting error as below.
Warning: Cannot update a component from inside the function body of a
different component.
How to remove this warning? What I am doing wrong here?
For me I was dispatching to my redux store in a React Hook. I had to dispatch in a useEffect to properly sync with the React render cycle:
export const useOrderbookSubscription = marketId => {
const { data, error, loading } = useSubscription(ORDERBOOK_SUBSCRIPTION, {
variables: {
marketId,
},
})
const formattedData = useMemo(() => {
// DISPATCHING HERE CAUSED THE WARNING
}, [data])
// DISPATCHING HERE CAUSED THE WARNING TOO
// Note: Dispatching to the store has to be done in a useEffect so that React
// can sync the update with the render cycle otherwise it causes the message:
// `Warning: Cannot update a component from inside the function body of a different component.`
useEffect(() => {
orderbookStore.dispatch(setOrderbookData(formattedData))
}, [formattedData])
return { data: formattedData, error, loading }
}
If your code calls a function in a parent component upon a condition being met like this:
const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
const { data, loading, error } = useQuery(QUERY);
if (data && data.users.length === 0) {
return handleNoUsersLoaded();
}
return (
<div>
<p>Users are loaded.</p>
</div>
);
};
Try wrapping the condition in a useEffect:
const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
const { data, loading, error } = useQuery(QUERY);
useEffect(() => {
if (data && data.users.length === 0) {
return handleNoUsersLoaded();
}
}, [data, handleNoUsersLoaded]);
return (
<div>
<p>Users are loaded.</p>
</div>
);
};
It seems that you have latest build of React#16.13.x. You can find more details about it here. It is specified that you should not setState of another component from other component.
from the docs:
It is supported to call setState during render, but only for the same component. If you call setState during a render on a different component, you will now see a warning:
Warning: Cannot update a component from inside the function body of a different component.
This warning will help you find application bugs caused by unintentional state changes. In the rare case that you intentionally want to change the state of another component as a result of rendering, you can wrap the setState call into useEffect.
Coming to the actual question.
I think there is no need of getDerivedStateFromProps in the child component body. If you want to trigger the bound event. Then you can call it via the onClick of the Child component as i can see it is a <button/>.
class Child extends React.Component {
constructor(props){
super(props);
this.updateState = this.updateState.bind(this);
}
updateState() { // call this onClick to trigger the update
if (somecondition) {
// doing some redux store update
this.props.reloadApp();
}
}
render() {
return <button onClick={this.updateState} />;
}
}
Same error but different scenario
tl;dr wrapping state update in setTimeout fixes it.
This scenarios was causing the issue which IMO is a valid use case.
const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
setSomeState(someNewValue);
}).current;
return (
<SomeComponent onSomeUpdate={doUpdate} />
);
fix
const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
setTimeout(() => {
setSomeState(someNewValue);
}, 0);
}).current;
return (
<SomeComponent onSomeUpdate={doUpdate} />
);
In my case I had missed the arrow function ()=>{}
Instead of onDismiss={()=>{/*do something*/}}
I had it as onDismiss={/*do something*/}
I had same issue after upgrading react and react native, i just solved that issue by putting my props.navigation.setOptions to in useEffect. If someone is facing same problen that i had i just want to suggest him put your state changing or whatever inside useEffect
Commented some lines of code, but this issue is solvable :) This warnings occur because you are synchronously calling reloadApp inside other class, defer the call to componentDidMount().
import React from "react";
export default class App extends React.Component {
reloadHandler = () => {
console.log("[App] reloadComponent");
// this.props.onShowSpinner();
// this.props.onRefresh();
};
render() {
return <Child reloadApp={this.reloadHandler} />;
}
}
class Child extends React.Component {
static getDerivedStateFromProps(props, state) {
// if (somecondition) {
// doing some redux store update
props.reloadApp();
// }
}
componentDidMount(props) {
if (props) {
props.reloadApp();
}
}
render() {
return <h1>This is a child.</h1>;
}
}
I got this error using redux to hold swiperIndex with react-native-swiper
Fixed it by putting changeSwiperIndex into a timeout
I got the following for a react native project while calling navigation between screens.
Warning: Cannot update a component from inside the function body of a different component.
I thought it was because I was using TouchableOpacity. This is not an issue of using Pressable, Button, or TouchableOpacity. When I got the error message my code for calling the ChatRoom screen from the home screen was the following:
const HomeScreen = ({navigation}) => {
return (<View> <Button title = {'Chats'} onPress = { navigation.navigate('ChatRoom')} <View>) }
The resulting behavior was that the code gave out that warning and I couldn't go back to the previous HomeScreen and reuse the button to navigate to the ChatRoom. The solution to that was doing the onPress in an inline anonymous function.
onPress{ () => navigation.navigate('ChatRoom')}
instead of the previous
onPress{ navigation.navigate('ChatRoom')}
so now as expected behavior, I can go from Home to ChatRoom and back again with a reusable button.
PS: 1st answer ever in StackOverflow. Still learning community etiquette. Let me know what I can improve in answering better. Thanx
If you want to invoke some function passed as props automatically from child component then best place is componentDidMount lifecycle methods in case of class components or useEffect hooks in case of functional components as at this point component is fully created and also mounted.
I was running into this problem writing a filter component with a few text boxes that allows the user to limit the items in a list within another component. I was tracking my filtered items in Redux state. This solution is essentially that of #Rajnikant; with some sample code.
I received the warning because of following. Note the props.setFilteredItems in the render function.
import {setFilteredItems} from './myActions';
const myFilters = props => {
const [nameFilter, setNameFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
const filterName = record => record.name.startsWith(nameFilter);
const filterCity = record => record.city.startsWith(cityFilter);
const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
props.setFilteredItems(selectedRecords); // <-- Danger! Updates Redux during a render!
return <div>
<input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
<input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
</div>
};
const mapStateToProps = state => ({
records: state.stuff.items,
filteredItems: state.stuff.filteredItems
});
const mapDispatchToProps = { setFilteredItems };
export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
When I ran this code with React 16.12.0, I received the warning listed in the topic of this thread in my browser console. Based on the stack trace, the offending line was my props.setFilteredItems invocation within the render function. So I simply enclosed the filter invocations and state change in a useEffect as below.
import {setFilteredItems} from './myActions';
const myFilters = props => {
const [nameFilter, setNameFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
useEffect(() => {
const filterName = record => record.name.startsWith(nameFilter);
const filterCity = record => record.city.startsWith(cityFilter);
const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
props.setFilteredItems(selectedRecords); // <-- OK now; effect runs outside of render.
}, [nameFilter, cityFilter]);
return <div>
<input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
<input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
</div>
};
const mapStateToProps = state => ({
records: state.stuff.items,
filteredItems: state.stuff.filteredItems
});
const mapDispatchToProps = { setFilteredItems };
export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
When I first added the useEffect I blew the top off the stack since every invocation of useEffect caused state change. I had to add an array of skipping effects so that the effect only ran when the filter fields themselves changed.
I suggest looking at video below. As the warning in the OP's question suggests, there's a change detection issue with the parent (Parent) attempting to update one child's (Child 2) attribute prematurely as the result of another sibling child's (Child 1) callback to the parent. For me, Child 2 was prematurely/incorrectly calling the passed in Parent callback thus throwing the warning.
Note, this commuincation workflow is only an option. I personally prefer exchange and update of data between components via a shared Redux store. However, sometimes it's overkill. The video suggests a clean alternative where the children are 'dumb' and only converse via props mand callbacks.
Also note, If the callback is invoked on an Child 1 'event' like a button click it'll work since, by then, the children have been updated. No need for timeouts, useEffects, etc. UseState will suffice for this narrow scenario.
Here's the link (thanks Masoud):
https://www.youtube.com/watch?v=Qf68sssXPtM
In react native, if you change the state yourself in the code using a hot-reload I found out I get this error, but using a button to change the state made the error go away.
However wrapping my useEffect content in a :
setTimeout(() => {
//....
}, 0);
Worked even for hot-reloading but I don't want a stupid setTimeout for no reason so I removed it and found out changing it via code works just fine!
I was updating state in multiple child components simultaneously which was causing unexpected behavior. replacing useState with useRef hook worked for me.
Try to use setTimeout,when I call props.showNotification without setTimeout, this error appear, maybe everything run inTime in life circle, UI cannot update.
const showNotifyTimeout = setTimeout(() => {
this.props.showNotification();
clearTimeout(showNotifyTimeout);
}, 100);
As per the docs:
When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext.
...
A component calling useContext will always re-render when the context value changes.
In my Gatsby JS project I define my Context as such:
Context.js
import React from "react"
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {},
}
const Context = React.createContext(defaultContextValue)
class ContextProviderComponent extends React.Component {
constructor() {
super()
this.setData = this.setData.bind(this)
this.state = {
...defaultContextValue,
set: this.setData,
}
}
setData(newData) {
this.setState(state => ({
data: {
...state.data,
...newData,
},
}))
}
render() {
return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>
}
}
export { Context as default, ContextProviderComponent }
In a layout.js file that wraps around several components I place the context provider:
Layout.js:
import React from 'react'
import { ContextProviderComponent } from '../../context'
const Layout = ({children}) => {
return(
<React.Fragment>
<ContextProviderComponent>
{children}
</ContextProviderComponent>
</React.Fragment>
)
}
And in the component that I wish to consume the context in:
import React, { useContext } from 'react'
import Context from '../../../context'
const Visuals = () => {
const filterByYear = 'year'
const filterByTheme = 'theme'
const value = useContext(Context)
const { filterBy, isOptionClicked, filterValue } = value.data
const data = <<returns some data from backend>>
const works = filterBy === filterByYear ?
data.nodes.filter(node => node.year === filterValue)
:
data.nodes.filter(node => node.category === filterValue)
return (
<Layout noFooter="true">
<Context.Consumer>
{({ data, set }) => (
<div onClick={() => set( { filterBy: 'theme' })}>
{ data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1> }
</div>
)
</Context.Consumer>
</Layout>
)
Context.Consumer works properly in that it successfully updates and reflects changes to the context. However as seen in the code, I would like to have access to updated context values in other parts of the component i.e outside the return function where Context.Consumer is used exclusively. I assumed using the useContext hook would help with this as my component would be re-rendered with new values from context every time the div is clicked - however this is not the case. Any help figuring out why this is would be appreciated.
TL;DR: <Context.Consumer> updates and reflects changes to the context from child component, useContext does not although the component needs it to.
UPDATE:
I have now figured out that useContext will read from the default context value passed to createContext and will essentially operate independently of Context.Provider. That is what is happening here, Context.Provider includes a method that modifies state whereas the default context value does not. My challenge now is figuring out a way to include a function in the default context value that can modify other properties of that value. As it stands:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {}
}
set is an empty function which is defined in the ContextProviderComponent (see above). How can I (if possible) define it directly in the context value so that:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
test: 'hi',
set: (newData) => {
//directly modify defaultContextValue.data with newData
}
}
There is no need for you to use both <Context.Consumer> and the useContext hook.
By using the useContext hook you are getting access to the value stored in Context.
Regarding your specific example, a better way to consume the Context within your Visuals component would be as follows:
import React, { useContext } from "react";
import Context from "./context";
const Visuals = () => {
const filterByYear = "year";
const filterByTheme = "theme";
const { data, set } = useContext(Context);
const { filterBy, isOptionClicked, filterValue } = data;
const works =
filterBy === filterByYear
? "filter nodes by year"
: "filter nodes by theme";
return (
<div noFooter="true">
<div>
{data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1>}
the value for the 'works' variable is: {works}
<button onClick={() => set({ filterBy: "theme" })}>
Filter by theme
</button>
<button onClick={() => set({ filterBy: "year" })}>
Filter by year
</button>
</div>
</div>
);
};
export default Visuals;
Also, it seems that you are not using the works variable in your component which could be another reason for you not getting the desired results.
You can view a working example with the above implementation of useContext that is somewhat similar to your example in this sandbox
hope this helps.
Problem was embarrassingly simple - <Visuals> was higher up in the component tree than <Layout was for some reason I'm still trying to work out. Marking Itai's answer as correct because it came closest to figuring things out giving the circumstances
In addition to the solution cited by Itai, I believe my problem can help other people here
In my case I found something that had already happened to me, but that now presented itself with this other symptom, of not re-rendering the views that depend on a state stored in a context.
This is because there is a difference in dates between the host and the device. Explained here: https://github.com/facebook/react-native/issues/27008#issuecomment-592048282
And that has to do with the other symptom that I found earlier: https://stackoverflow.com/a/63800388/10947848
To solve this problem, just follow the steps in the first link, or if you find it necessary to just disable the debug mode
I am working to build a Pokedex from JSON data in React. I am refactoring this project from one I built in jQuery, so it could be that the jQuery approach is causing me to misunderstand how to approach this problem with proper React thinking. What's tripping me up so far is how to dynamically render multiple child elements based on the JSON I pass from a the parent element (this would be jQuery append).
Here is my App.js code:
class App extends Component {
render() {
return (
<div className="App background">
<div className="content">
<Header />
<TilesContainer pokedexName="national"/>
</div>
</div>
);
}
The TilesContainer essentially receives the name of a Pokedex and makes a call to an API. The individual Pokemon names are stored in an array in the TilesContainer state (this.state.pokemon), as below.
class TilesContainer extends Component {
constructor(props){
super(props);
this.state = {pokemon: []};
this.getPokemon = this.getPokemon.bind(this);
this.tiles = this.tiles.bind(this);
}
getPokemon() {
// set this.state.pokemon to the list
let link = 'https://pokeapi.co/api/v2/pokedex/' + this.props.pokedexName + '/';
fetch(link)
.then(response => response.json())
.then(myJson => {
let list = myJson['pokemon_entries'];
list.forEach(pokemon => {
this.state.pokemon.push(pokemon);
})
})
this.tiles();
}
tiles() {
if (this.state.pokemon.length > 0) {
return (
this.state.pokemon.map(pokemon => {
<Tile number={pokemon.entry_number}/>
})
)
}
}
render(){
this.getPokemon();
return (
<div id="tiles-container"
className="tiles-container">
<h1>TilesContainer Test</h1>
<Tile number={1} />
</div>
)
}
}
export default TilesContainer
Again, the idea is that a Pokemon tile is render for each Pokemon in the Pokedex JSON (which for now I've stored in this.state.pokemon - not sure if this is the best approach). I found an example here on Stack Overflow that uses an additional function (this this case this.tiles() to generate what I think is an array of returns with different child elements). The <Tile number={1} /> is a hardcoded example of how the tile is called.
Currently no dynamically-rendered tiles show up when the code runs. Is this the correct approach. I'd really appreciate any suggestions.
Thanks!
It looks like you're almost there.
First off, never modify state directly. Use this.setState() instead. State in React is updated asynchronously. For your purposes, you should be able to modify getPokemon() like the following. I also removed the this.tiles() call, as it is unnecessary.
getPokemon() {
// set this.state.pokemon to the list
let link = 'https://pokeapi.co/api/v2/pokedex/' + this.props.pokedexName + '/';
fetch(link)
.then(response => response.json())
.then(myJson => {
let list = myJson['pokemon_entries'];
this.setState({
pokemon: list,
});
})
}
A minor correction for tiles(): when using an arrow function and returning something in one line, use parentheses instead of curly braces. When you use curly braces, you have to include a return statement. With parentheses, you do not.
tiles() {
if (this.state.pokemon.length > 0) {
return (
this.state.pokemon.map(pokemon => (
<Tile number={pokemon.entry_number}/>
))
)
}
}
Next, since tiles() returns your dynamic tile components, it needs to be included in what you return in render().
render(){
return (
<div id="tiles-container"
className="tiles-container"
>
<h1>TilesContainer Test</h1>
{this.tiles()}
</div>
)
}
Lastly, I think the call to this.getPokemon() would make more sense in the constructor, rather than in render().
I think your method of getting the json data and storing it in state is fine, by the way. In the future, you may want to look into Redux to manage your state, but it could be overkill for a really small application.
so you are passing the pokedexName from the parent component which is app.js, once you get the props you can call the rest api call on the componentWillMount life cycle.
so on the render since the api call has been initiated it wont have any data thats why we are using a ternary operator to check the array once the api call get finished and we get the data we are setting the data to the pokemon array.
Since the state is updated react will automatically render a re render so the data will appear.
i hope the below code will solve the issue, please let me know :)
// App.js
import React, { Component } from 'react';
import TilesContainer from './components/TileContainer/TilesContainer'
class App extends Component {
render() {
return (
<div>
<TilesContainer pokedexName="national" />
</div>
);
}
}
export default App;
// Tiles container
import React, {Component} from 'react';
import axios from 'axios';
class TilesContainer extends Component{
//state
state ={
pokemon: []
}
// life cycle methods
componentWillMount(){
let link = 'https://pokeapi.co/api/v2/pokedex/' + this.props.pokedexName + '/';
axios.get(link)
.then(res => {
this.setState({
pokemon: res.data["pokemon_entries"]
})
})
}
render(){
let style ={display:"inline"}
return(
<div>
{
this.state.pokemon.length > 0 ?
this.state.pokemon.map(pokemon => {
return(
<div key={pokemon.entry_number}>
<p style={style}>{pokemon.entry_number}</p>
<a href={pokemon.pokemon_species.url}>{pokemon.pokemon_species.name}</a>
</div>
)
})
:
null
}
</div>
)
}
}
export default TilesContainer
I have the usual problem with infinite loop and I don't know why.
Im using reactJS 16.5.2
The loops generally occurs when you write a SetState where not allowed (for example in render method).
Im following this guide: https://medium.com/#baphemot/understanding-reactjs-component-life-cycle-823a640b3e8d
to pay attention about this issue.
I made several HOC(Decorators/Wrapper) components to concentrate general purpose methods in one point using props to propagate them to every children.
It generally works perfectly.
I tried to simplify my components structure below.
The problem is the FORM and its children.
One of the input has a DropDown that has to be populated with a method of the upper Wrapper. I put the call in componentDidMount(as the link above suggest). Unfortunally the wrapper setState seems to trigger a complete descrution and re-building of FORM Component. I put a console.log in every constructor from Wrapped to the form. Only the FORM and all its INPUTS are reacreated (and not updated).
This recreation generates an infinite loop because componentDidMountis triggered everytime.
I don't know how to fix this. I've checked every "key" properties and ALL components has their unique keys. I'm asking you WHY react recreate instead of update?
Is due to the form building method in parent render? And if so, which is the right design pattern to build a form with Async data population?
Simplify your life and instead of creating a bunch of wrappers, just create a single container-component that'll function the same way. For example, you would create a container that cares about data and state, then shares it and its methods with a reusable child component (as shown below, both function the same).
This would work exactly the same way with data fetched from an API. You'll retrieve data in componentDidMount, set it state, then pass down the state to the reuseable component.
You can get super granular with your reusable components. For example a reusable button that's sole purpose is to submit a form. Or a reusable input that only captures numbers between 1 and 100 and so on.
If your components are heavily nested, then consider using redux.
Working example: https://codesandbox.io/s/x2ol8wmzrp
containers/Form.js (container-component)
import React, { Component } from "react";
import Fields from "../components/Fields";
export default class Form extends Component {
state = {
buttonFields: [
{ id: "Apples", quantity: 1 },
{ id: "Strawberries", quantity: 1 },
{ id: "Grapes", quantity: 1 },
{ id: "Apricots", quantity: 1 }
]
};
handleButtonClick = id => {
this.setState(prevState => ({
buttonFields: prevState.buttonFields.map(
item =>
id === item.id ? { id, quantity: item.quantity + 1 } : { ...item }
)
}));
};
render = () => (
<Fields
{...this.state}
onButtonClick={this.handleButtonClick}
title="Container Component"
/>
);
}
components/Fields.js (reusable component)
import React from "react";
export default ({ buttonFields, onButtonClick, title }) => (
<div className="container">
<h1 style={{ textAlign: "center" }}>{title}</h1>
{buttonFields.map(({ id, quantity }) => (
<button
style={{ marginRight: 10 }}
className="uk-button uk-button-primary"
key={id}
onClick={() => onButtonClick(id)}
>
{id} ({quantity})
</button>
))}
</div>
);
containers/Wrapper.js (unnecessary wrapper)
import React, { Component } from "react";
export default WrappedComponent => {
class Form extends Component {
state = {
buttonFields: [
{ id: "Apples", quantity: 1 },
{ id: "Strawberries", quantity: 1 },
{ id: "Grapes", quantity: 1 },
{ id: "Apricots", quantity: 1 }
]
};
handleButtonClick = id => {
this.setState(prevState => ({
buttonFields: prevState.buttonFields.map(
item =>
id === item.id ? { id, quantity: item.quantity + 1 } : { ...item }
)
}));
};
render = () => (
<WrappedComponent
{...this.state}
onButtonClick={this.handleButtonClick}
title="Wrapper"
/>
);
}
return Form;
};
Thanking Matt Carlotta for his answer, I figure out what was the problem.
In the image above I simplified too much so I missed one important declaration.
In "FinalComponent" when I was creating the SomeFormComponent, due to its wrapping, I was doing something like this:
renderForm()
{
var WrappedFormComponent = FormHOC(SomeFormComponent();
return <WrappedFormComponent {...this.props} [...] />
}
It's obvious that with that syntax, the Form is instantatied every time due to renderForm method called in render method.
The solution is very simple. I moved that line above the component:
const WrappedFormComponent = FormHOC(SomeFormComponent();
export default class FinalComponent extends React.Component