React functional component not updating on setState from parent component - javascript

My React component uses apollo to fetch data via graphql
class PopUpForm extends React.Component {
constructor () {
super()
this.state = {
shoptitle: "UpdateMe",
popupbodyDesc: "UpdateMe"
}
}
render()
{
return (
<>
<Query query={STORE_META}>
{({ data, loading, error, refetch }) => {
if (loading) return <div>Loading…</div>;
if (error) return <div>{error.message}</div>;
if (!data) return (
<p>Could not find metafields :(</p>
);
console.log(data);
//loop over data
var loopedmetafields = data.shop.metafields.edges
console.log(loopedmetafields)
loopedmetafields.forEach(element => {
console.log(element.node.value)
if (element.node.value === "ExtraShopDescription"){
this.setState({
shoptitle: element.node.value
});
console.log(this.state.shoptitle)
}
if (element.node.value === "bodyDesc"){
this.setState({
popupbodyDesc: element.node.value
});
console.log(this.state.popupbodyDesc)
}
});
return (
<>
<AddTodo mkey="ExtraShopDesc" namespace="ExtraShopDescription" desc={this.state.shoptitle} onUpdate={refetch} />
<AddTodo mkey="body" namespace="bodyDesc" desc={this.state.popupbodyDesc} onUpdate={refetch} />
</>
);
}}
</Query>
</>
)
}
}
export default PopUpForm
Frustratingly the functional component renders before the state is set from the query. Ideally the functional component would only render after this as I thought was baked into the apollo library but seems I was mistaken and it seems to execute synchronous rather than asynchronous
As you can see I pass the props to the child component, in the child component I use these to show the current value that someone might amend
The functional component is here
function AddTodo(props) {
let input;
const [desc, setDesc] = useState(props.desc);
//console.log(desc)
useEffect( () => {
console.log('props updated');
console.log(props)
}, [props.desc])
const [addTodo, { data, loading, error }] = useMutation(UPDATE_TEXT, {
refetchQueries: [
'STORE_META' // Query name
],
});
//console.log(data)
if (loading) return 'Submitting...';
if (error) return `Submission error! ${error.message}`;
return (
<div>
<form
onSubmit={e => {
console.log(input.value)
setDesc(input.value)
e.preventDefault();
const newmetafields = {
key: props.mkey,
namespace: props.namespace,
ownerId: "gid://shopify/Shop/55595073672",
type: "single_line_text_field",
value: input.value
}
addTodo({ variables: { metafields: newmetafields } });
input.value = input.value
}}
>
<p>This field denotes the title of your pop-up</p>
<input className="titleInput" defaultValue={desc}
ref={node => {
input = node;
}}
/>
<button className="buttonClick" type="submit">Update</button>
</form>
</div>
);
}
Now I need this component to update when the setState is called on PopUpForm
Another stack overflow answer here gives me some clues
Passing the intial state to a component as a prop is an anti-pattern
because the getInitialState (in our case the constuctor) method is
only called the first time the component renders. Never more. Meaning
that, if you re-render that component passing a different value as a
prop, the component will not react accordingly, because the component
will keep the state from the first time it was rendered. It's very
error prone.
Hence why I then implemented useEffect however the console.log in useEffect is still "updateMe" and not the value as returned from the graphql call.
So where I'm at
I need the render the functional component after the the grapql call
and I've manipulated the data, this seems to be the best approach in terms of design patterns also
or
I need setState to pass/render the functional component with the new value
As an aside if I do this
<AddTodo mkey="ExtraShopDesc" namespace="ExtraShopDescription" desc={data.shop.metafields.edges[0].node.value} onUpdate={refetch} />
It will work but I can't always expect the value to be 0 or 1 as metafields might have already defined

I think there is a simpler way than using setState to solve this. You can for example use find like this:
const shopTitleElement = loopedmetafields.find(element => {
return element.node.value === "ExtraShopDescription"
})
const shopBodyElement = loopedmetafields.find(element => {
return element.node.value === "bodyDesc"
});
return (
<>
<AddTodo mkey="ExtraShopDesc" namespace="ExtraShopDescription" desc={shopTitleElement.node.value} onUpdate={refetch} />
<AddTodo mkey="body" namespace="bodyDesc" desc={shopBodyElement.node.value} onUpdate={refetch} />
</>
);

Related

React how to call componentDidMount every time when switching between ListItem

When initializing widget component which loads various charts it works as it should, but when switching to another ListItem , componentDidMount do not load when switch to another item. I need to load it, because it fetch required data for it. But the thing is when I am switching between ListItem did not initialize componentDidMount
DashboardSidebar.jsx
class DashboardSidebar extends Component {
constructor(props) {
super(props);
this.state = {
tabLocation: this.props.tabLocation
};
this.onChange = this.onChange.bind(this);
}
onChange(event) {
this.setState({
tabLocation: event.target.value
});
}
render() {
const { classes } = this.props;
const { path } = this.props.match;
const { reports = [], sites = [] } = this.props;
let fromFilterString = "-"
let toFilterString = "-"
return (
<Drawer
className={classes.drawer}
variant="permanent"
classes={{
paper: classes.drawerPaper,
}}>
<div>
<List>
{reports.map((report) => (
<ListItem
onClick={this.onChange} button key={report.id}>
<ListItemIcon>
<DashboardIcon />
</ListItemIcon>
<ListItemText
disableTypography
primary={
<Typography type="body2">
<Link to={`${path}/${report.slug}`} style={{ color: "#000" }}>
{report.name}
</Link>
</Typography>
}
/>
</ListItem>
))}
</List>
</div>
<Divider light />
</Drawer>
)
}
}
This component seems run correct, but when clicking on other ListItem run componentDidUpdate which do not fetch required data for charts. Also I find out that when I changed in MainDashboard component key={i} to key={l.id} is started to hit componentDidMount, but then widget's do not load, but from Console I can see that it hit componentDidMount and fetch data which I console.log() .
MainDashboard.jsx
class MainDashboard extends Component {
componentDidUpdate(prevProps) {
if (this.props.match.params.report === prevProps.match.params.report) {
return true;
}
let widgets = {};
let data = {};
let layout = {};
fetch(...)
.then(response => response.json())
.then(data => {
...
this.setState({dashboard: data, isLoading: false, layouts: layout });
})
}
componentDidMount() {
this.setState({ mounted: true, isLoading: true });
let widgets = {};
let data = {};
let layout = {};
fetch(...
})
.then(response => response.json())
.then(data => {
...
this.setState({dashboard: data, isLoading: false, layouts: layout });
})
}
sortWidgets(widgets) {
...
return widgets;
}
generateDOM() {
return _.map(this.state.dashboard.widgets, function(l, i) {
....
return (
<div key={i}>
<ChartWidget
visualization={l.visualization}
name={l.visualization.name}
</div>
);
}.bind(this));
}
render() {
return (
...
);
}
}
You have three option:
1- Use another life cycle such as componentDidUpdate, fetch the new data if there is particular change in props or state
2- Use a new Key, if you use a new key on a component, it will trigger a componentDidMount, because the component is rendered again as a new entity
3- Use react.ref
I think you should read up on all three choices and choose one that will fit you the best.
So componentDidMount is actually being hit but nothing is happening in terms of data updates? in that case, your component loads/mounts first and whatever needs to happen doesn't trigger a re-render, I would look into using another lifecycle method to ensure that your data is updated.
I'm not sure if you're working on a legacy system but if upgrading to functional components is an option, I would recommend using the lifecycle method useEffect because it replaces many lifecycle methods like componentDidMount , componentDidUpdate and the unsafe componentWillUnmount and will make your life a whole lot easier and more efficient.
componentDidMount is only executed when a component is mounted.
A state update in DashboardSidebar would not cause BaseDashboard to be re-mounted so componentDidMount will not be re-executed for BaseDashboard.
Have you tried fetching the data in the onChange event handler (for switching to another ListItem) instead?

Getting a Objects are not valid as a React child error even though I am not trying to render an object

As the title states, I am making an api request, and then returning a component with the contents of the api request as the prop. However, idk why I keep getting this error.
App.js
showPicture = async () =>{
//console.log(KEY)
const response = await picOfTheDay.get('',{
params: {
date:this.date,
hd: true,
api_key: 'KEY'
}
});
//console.log()
this.setState({picDayFullDescription: response}, ()=>{
return <PictureOfTheDay date = {this.state.date} picture= {this.state.picDayFullDescription.url} description={this.state.picDayFullDescription.explanation} />
})
}
render(){
return(
<div>
{/* <PictureOfTheDay date = {this.state.date} picture= {this.state.picDayFullDescription.url} description={this.state.picDayFullDescription.explanation}/> */}
{this.showPicture()}
</div>
)
}
PictureOfTheDay.js
class PictureOfTheDay extends React.Component{
constructor(props){
super(props);
}
render(){
return(
<div>
Hello?
</div>
)
}
}
Can someone please point me to the right direction
Instead of calling the function in the render, I would rather put the component on the render and then call the fetch function on some lifecycle hook like componentDidMount.
This updates the state, hence re-rendering the component and the PictureOfTheDay... If the component does not work with an empty description etc which might be a cause of you wanting to make sure the fields are there, render it conditionally based on the needed information e.g {this.state.picDayFullDescription && ...}
// App.js
componentDidMount() {
this.showPicture();
}
showPicture = async () => {
const response = await picOfTheDay.get("", {
params: {
date: this.date,
hd: true,
api_key: "KEY",
},
});
this.setState({ picDayFullDescription: response });
}
render() {
return (
<div>
<PictureOfTheDay
date={this.state.date}
picture={this.state.picDayFullDescription.url}
description={this.state.picDayFullDescription.explanation}
/>
</div>
);
}

How to access ref of a component through a function in React Native?

I've imported a custom component into my screen and rendered it in the render() function. Then, created a ref to that custom component. Now, the render() function simply looks like this.
render() {
return (
<View>
<MyComponent ref={component => this.myComponent1 = component} />
<MyComponent ref={component => this.myComponent2 = component} />
<MyComponent ref={component => this.myComponent3 = component} />
</View>
)
}
Then, In the same screen file, I've created another function to access the state of my custom component. I wrote it like this.
myFunction = (ref) => {
ref.setState({ myState: myValue })
}
Then, I want to call that function for those separate components separately like this. (In the screen file)
this.myFunction(this.myComponent1)
this.myFunction(this.myComponent2)
this.myFunction(this.myComponent3)
But, it does not work. It gives me the following error.
null is not an object (evaluating 'ref.setState')
Actually what I need this myFunction to do is,
this.myComponent1.setState({ myState: myValue })
this.myComponent2.setState({ myState: myValue })
this.myComponent3.setState({ myState: myValue })
The state myState is in the component while I want to access it through the myFunction() in my screen file.
Can you please help me to solve this problem?
This is not good practice to setState of child component from parent component.
I am assuming you want to set some value to your child component's state by trying this approach.
You can keep these values in your local state and pass it to props and your child component will re-render / get updated value there.
class Component {
constructor() {
this.state = {
myValues: {
component1: "abc",
component2: "xyz",
component3: "123",
}
}
}
myFunction(componentName, newValue) {
this.setState({
myValues: {
...this.state.myValues,
[componentName]: newValue
}
})
}
render() {
return (
<View>
<MyComponent value={this.state.myValues.component1} />
<MyComponent value={this.state.myValues.component2} />
<MyComponent value={this.state.myValues.component3} />
</View>
)
}
};
First make sur that MyComponent is a component and not a stateless Component, then, to change states, try this :
myFunction = (ref) => {
ref.current.setState({ myState: myValue }
}
and of course , for it to work, your component need to be already mounts, If you try for example to call this in the constructor, it will not work
Inside your component, in the componentDidMount function please add
componentDidMount() {
this.props.refs(this)
}
setState(key, value){
this.setState({[key]:value})
}
and please change the ref param to refs
<MyComponent refs={component => this.myComponent1 = component} />
myFunction = (ref) => {
ref.setState('myState', 'myValue')
}

Component is somehow rendering despite conditional statement

On my Home component, on initial load, I loop through an object of URLs, and use Promise.all to make sure they all resolve at the same time. I construct an object, and push it into state. At the start of this I have loading: true, then set to false when done pushing that object into state:
Home Component:
class Home extends Component {
state = {
searchTerm: null,
movies: {
trending: {},
topRated: {},
nowPlaying: {},
upcoming: {}
},
loading: false
};
componentDidMount() {
this.getInitalMovies();
}
getInitalMovies = () => {
const API_KEY = process.env.REACT_APP_API_KEY;
//set loading to true
this.setState({ loading: true });
//create an object with all movie URLs
const allMovieURLs = {
trending: `https://api.themoviedb.org/3/trending/movie/day?api_key=${API_KEY}`,
topRated: `https://api.themoviedb.org/3/movie/top_rated?api_key=${API_KEY}&language=en-US&page=1`,
nowPlaying: `https://api.themoviedb.org/3/movie/now_playing?api_key=${API_KEY}&language=en-US&page=1`,
upcoming: `https://api.themoviedb.org/3/movie/upcoming?api_key=${API_KEY}&language=en-US&page=1`
};
//break down the movieURL object into entries, fetch the URL, and reassign entries with actual data
//encapsulate within a Promise.all to ensure they all resolve at the same time.
const moviePromises = Promise.all(
Object.entries(allMovieURLs).map(entry => {
const [key, url] = entry;
return fetch(url).then(res => res.json().then(data => [key, data]));
})
);
//with the returned promise from Promise.all, reconstruct the array of entries back into an object with relevant key pair values
const movies = moviePromises.then(movieArr => {
const dataObj = {};
for (const [movie, movieData] of movieArr) {
dataObj[movie] = movieData;
}
return dataObj;
});
//with the returned object, push it into current state, then turn off loading
movies.then(movieObj =>
this.setState({ movies: movieObj, loading: false })
);
};
render() {
const { movies } = this.state;
return (
<div className='App'>
<Header
submitHandler={this.submitHandler}
changeHandler={this.changeHandler}
/>
<HeroImage />
{this.state.loading ? <Loader /> : <MovieDisplay movies={movies} />}
</div>
);
}
MovieDisplay Component:
export default class MovieDisplay extends Component {
render() {
const { movies } = this.props;
return (
<div>
<MovieRow movies={movies.trending.results} movieType='trending' />
<MovieRow movies={movies.topRated.results} movieType='top rated' />
<MovieRow movies={movies.nowPlaying.results} movieType='now playing' />
<MovieRow movies={movies.upcoming.results} movieType='upcoming' />
</div>
);
}
}
MovieRow Component:
export default class MovieRow extends Component {
render() {
const { movieType, movies } = this.props;
return (
<div>
<div className='row-title'>{movieType}</div>
{console.log(movies)} //This part seems to somehow mount even when conditional rendering says it shouldn't!
<Slider {...settings} />
</div>
);
}
}
I then have the body do a conditional render like so, so that if the loading is complete (loading: false), then my MovieDisplay component should render, otherwise it's still loading, so show the Loader component.
I confimed that this part is working (if I search the React Devtools for Loader when loading: false it does not exist, but MovieDisplay does exist.
I'm passing down a data object via props from Home > MovieDisplay > MovieRow, and then looping through the array to display more components.
However, on initial load, it seems the MovieRow (last, nested child component) is somehow being mounted for a quick second, because in the console it's logging 4 undefined statements briefly, before resolving with the proper data.
Main question: If the Parent Component is not rendered to the DOM, then the child components inside of the Parent should also not be rendered, right?
Secondary question: Is it possible that all the components in my app are rendering briefly for a second on initial load, despite having a conditional in the render() function? That's the only thing I can think of that's causing this.
Example: If MovieDisplay is not rendered, then everything inside of it like MovieRow should also not be rendered, correct?
Hope this isn't too confusing...please let me know if I need to edit my problem or elaborate.
.then does not resolve a promise. It lets you get the value after the promise was resolved
This is because of the asynchronous nature of JS. Initially, when componentDidMount is called, you set loading = true.
Before the promise is completed(loading = true) react renders the HOME component, this is the reason it calls MovieDisplay component.
Try adding an extra condition where you call MovieDisplay
{this.state.loading && "check your data is filled in movies object" ? <Loader /> : <MovieDisplay movies={movies} />}
Can you try this:
{this.state.loading && <Loader />}
{!this.state.loading && <MovieDisplay movies={movies} />}

ReactJS instant Search with input

Im making my first react project. Im new in JS, HTML, CSS and even web app programming.
What i want to do it is a Search input label. Now its look like this:
Like you can see i have some list of objects and text input.
I Have two components, my ProjectList.js with Search.js component...
class ProjectsList extends Component {
render() {
return (
<div>
<Search projects={this.props.projects} />
<ListGroup>
{this.props.projects.map(project => {
return <Project project={project} key={project.id} />;
})}
</ListGroup>
</div>
);
}
}
export default ProjectsList;
... and ProjectList.js displays Project.js:
How looks Search.js (its not ended component)
class Search extends Component {
state = {
query: ""
};
handleInputChange = () => {
this.setState({
query: this.search.value
});
};
render() {
return (
<form>
<input
ref={input => (this.search = input)}
onChange={this.handleInputChange}
/>
<p />
</form>
);
}
}
export default Search;
My project have name property. Could you tell me how to code Search.js component poperly, to change displaying projects dynamically based on input in text label? for example, return Project only, if text from input match (i want to search it dynamically, when i start typing m... it shows all projects started on m etc).
How to make that Search input properly? How to make it to be universal, for example to Search in another list of objects? And how to get input from Search back to Parent component?
For now, in react dev tools whatever i type there i get length: 0
Thanks for any advices!
EDIT:
If needed, my Project.js component:
class Project extends Component {
state = {
showDetails: false
};
constructor(props) {
super(props);
this.state = {
showDetails: false
};
}
toggleShowProjects = () => {
this.setState(prevState => ({
showDetails: !prevState.showDetails
}));
};
render() {
return (
<ButtonToolbar>
<ListGroupItem className="spread">
{this.props.project.name}
</ListGroupItem>
<Button onClick={this.toggleShowProjects} bsStyle="primary">
Details
</Button>
{this.state.showDetails && (
<ProjectDetails project={this.props.project} />
)}
</ButtonToolbar>
);
}
}
export default Project;
To create a "generic" search box, perhaps you could do something like the following:
class Search extends React.Component {
componentDidMount() {
const { projects, filterProject, onUpdateProjects } = this.props;
onUpdateProjects(projects);
}
handleInputChange = (event) => {
const query = event.currentTarget.value;
const { projects, filterProject, onUpdateProjects } = this.props;
const filteredProjects = projects.filter(project => !query || filterProject(query, project));
onUpdateProjects(filteredProjects);
};
render() {
return (
<form>
<input onChange={this.handleInputChange} />
</form>
);
}
}
This revised version of Search takes some additional props which allows it to be reused as required. In addition to the projects prop, you also pass filterProject and onUpdateProjects callbacks which are provided by calling code. The filterProject callback allows you to provide custom filtering logic for each <Search/> component rendered. The onUpdateProjects callback basically returns the "filtered list" of projects, suitable for rendering in the parent component (ie <ProjectList/>).
The only other significant change here is the addition of visibleProjects to the state of <ProjectList/> which tracks the visible (ie filtered) projects from the original list of projects passed to <ProjectList/>:
class Project extends React.Component {
render() {
return (
<div>{ this.props.project }</div>
);
}
}
class ProjectsList extends React.Component {
componentWillMount() {
this.setState({ visibleProjects : [] })
}
render() {
return (
<div>
<Search projects={this.props.projects} filterProject={ (query,project) => (project == query) } onUpdateProjects={ projects => this.setState({ visibleProjects : projects }) } />
<div>
{this.state.visibleProjects.map(project => {
return <Project project={project} key={project.id} />;
})}
</div>
</div>
);
}
}
class Search extends React.Component {
componentDidMount() {
const { projects, filterProject, onUpdateProjects } = this.props;
onUpdateProjects(projects);
}
handleInputChange = (event) => {
const query = event.currentTarget.value;
const { projects, filterProject, onUpdateProjects } = this.props;
const filteredProjects = projects.filter(project => !query || filterProject(query, project));
onUpdateProjects(filteredProjects);
};
render() {
return (
<form>
<input onChange={this.handleInputChange} />
</form>
);
}
}
ReactDOM.render(
<ProjectsList projects={[0,1,2,3]} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I will assumes both your Search and ProjectList component have a common parent that contains the list of your projects.
If so, you should pass a function into your Search component props, your Search component will then call this function when the user typed something in the search bar. This will help your parent element decide what your ProjectsLists needs to render :
handleInputChange = () => {
this.props.userSearchInput(this.search.value);
this.setState({
query: this.search.value
});
};
And now, here is what the parent element needs to include :
searchChanged = searchString => {
const filteredProjects = this.state.projects.filter(project => project.name.includes(searchString))
this.setState({ filteredProjects })
}
With this function, you will filter out the projects that includes the string the user typed in their names, you will then only need to put this array in your state and pass it to your ProjectsList component props
You can find the documentation of the String includes function here
You can now add this function to the props of your Search component when creating it :
<Search userSearchInput={searchChanged}/>
And pass the filtered array into your ProjectsList props :
<ProjectsList projects={this.state.filteredProjects}/>
Side note : Try to avoid using refs, the onCHnage function will send an "event" object to your function, containing everything about what the user typed :
handleInputChange = event => {
const { value } = event.target
this.props.userSearchInput(value);
this.setState({
query: value
});
};
You can now remove the ref from your code

Categories

Resources