I have a problem with my code. I am trying to make a notes in react + fireabse. Adding notes to fireabse works and setState shows them, but if I want to change the value of the note, second setState does not change it but in firebase the note will change its value.
Here is my code
constructor() {
super();
this.app = firebase.initializeApp(DB_CONFIG);
this.database = this.app.database().ref().child('notes');
this.state = {
notes: [],
};
}
componentDidMount() {
this.database.on('child_added', snap => {
this.state.notes.push(new Note(snap.key, snap.val().noteContent));
this.setState({
notes: this.state.notes
});
});
this.database.on('child_changed', snap => {
this.state.notes.forEach(note => {
if(snap.key === note.id) {
note.id = snap.key;
note.noteContent = snap.val().noteContent;
}
});
this.setState({
notes: this.state.notes,
});
});
}
addNote(note) {
this.database.push().set({
noteContent: note,
});
}
changeNote(id, note) {
this.database.child(id).update({
noteContent: note,
});
}
render() {
return (
<div>
<div> {
this.state.notes.map(note => {
return (
<NoteComponent noteContent={note.noteContent}
noteId={note.id}
key={note.id}
changeNote={this.changeNote.bind(this)}>
</NoteComponent>
)
})
}
</div>
<div>
</div>
<NoteFormComponent
addNote={this.addNote.bind(this)}>
</NoteFormComponent>
</div>
);
}
Thanks for help.
Problem lines:
this.state.notes.push(new Note(snap.key, snap.val().noteContent));
or
this.state.notes.forEach(note => {
if(snap.key === note.id) {
note.id = snap.key;
note.noteContent = snap.val().noteContent;
}
});
this.setState({
notes: this.state.notes,
});
You cannot change the value of the state like this. You HAVE to use setState.
To fix this you need to:
copy(deepclone) the state array (You shoudl use ImmutableJS but this will work for testing: const copiedArray = JSON.parse(JSON.stringify(array)))
Do the changes to the copied array copiedArray.
setState('notes', copiedArray)
Other suggestions:
I would suggest to you to do the following. Isolate the firebase layer from the viewing layer. Mixing responsibilities of the component with the db communication is not recommanded.
After you do that. you will pass the list of note from outside the component. And any method working on the database(firebase in you case will come as a param also.)
// Notes Container
const notes = FireBaseConnector.getNotes();
const {
add,
edit,
delete,
} = FireBaseConnector
<Notes notes={notes}
onAdd={add}
onEdit={edit}
onRemove={delete}
/>
You should do something like this:
const newNotes = [...this.state.notes]
const newNode = {
id: snap.key,
noteContent: snap.val().noteContent,
}
newNotes.push(newNode)
this.setState({
notes: newNotes,
});
you cannot just push you need to replace the node array with the new array. immutability need to be followed to tell react to rerender
Related
I have two api requests that return JSON objects. They return an array of objects.
One API request that I make is fine and allows me to update the state with the response, but the other one (below) doesn't and I don't understand why.
API request to fetch genres list:
async getGenreList() {
const genresResults = await getGenres();
return genresResults;
}
The request:
export const getGenres = async () => {
try {
const response = await axios.get(
"https://api.themoviedb.org/3/genre/movie/list?api_key=<APIKEY>&language=en-US"
);
const { genres } = response.data;
return genres;
} catch (error) {
console.error(error);
}
};
The response is an array of 19 genre objects but this is just an example:
[
{id: 28, name: "Action"},
{id: 12, name: "Adventure"}
]
I then want to update the state like this and pass the response to genreOptions. But it tells me Error: Objects are not valid as a React child (found: object with keys {id, name}). If you meant to render a collection of children, use an array instead.
componentDidMount() {
this.getGenreList().then((response) => {
console.log(response)
this.setState({ genreOptions: response});
});
}
The below works when i update the state and map over it but I don't want to do that, i want to pass the whole response down so i can map over the data in my component as I need it there to do some data matching.
this.setState({ genreOptions: response.map((genreOption) => {
return genreOption.name
})});
This is the state:
this.state = {
results: [],
movieDetails: null,
genreOptions: [],
};
I want to pass the genreOptions here to genres then map over it in the MovieResults component.
<MovieResults>
{totalCount > 0 && <TotalCounter>{totalCount} results</TotalCounter>}
<MovieList movies={results || []} genres={genreOptions || []} />
</MovieResults>
Why can't I? Any ideas? I have done it for another similar request :S
UPDATE TO SHOW MOVIELIST COMPONENT
export default class MovieList extends React.Component {
render() {
const { movies, genres } = this.props;
const testFunction = (movieGenreIds) => {
const matchMovieGenresAndGenreIds = genres.map((genreId) => {
const matchedGenres = movieGenreIds.find((movieGenre) => {
return movieGenre.id === genreId
})
return matchedGenres // this returns the matching objects
})
const result = matchMovieGenresAndGenreIds.filter(Boolean).map((el)=> {
return el.name
})
return result
}
return (
<MoviesWrapper>
{movies.map((movie) => {
const {
title,
vote_average,
overview,
release_date,
poster_path,
genre_ids
} = movie;
return (
<MovieItem
title={title}
rating={vote_average}
overview={overview}
release={release_date}
poster={poster_path}
movieGenres={testFunction(genre_ids)}
/>
);
})}
</MoviesWrapper>
);
}
}
**** MOVIE ITEM COMPONENT***
export default class MovieItem extends React.Component {
render() {
const { title, overview, rating, release, poster, movieGenres } = this.props;
return (
// The MovieItemWrapper must be linked to the movie details popup
<MovieItemWrapper>
<LeftCont>
<img
className="movie-img"
src={`https://image.tmdb.org/t/p/w500${poster}`}
/>
</LeftCont>
<RightCont>
<div className="movie-title-container">
<h2 className="movie-title">{title}</h2>
<Rating>{rating}</Rating>
</div>
<div>{movieGenres}</div>
<p>{overview}</p>
<p>{release}</p>
</RightCont>
</MovieItemWrapper>
);
}
}
Please follow this steps to fix your code. I'll try yo explain what's happening along the way:
In your main component. Set the state to the value that you really want to pass to your child component. Remember that response will be an array of objects.
componentDidMount() {
this.getGenreList().then((response) => {
this.setState({genreOptions: response});
});
}
In your MovieList component. Please check your testFunction to respect data types. The following code will return you an array of strings containing the names of the genres that are included in the movies genres array.
const testFunction = (movieGenreIds) => {
return genres
.filter((genre) => {
return movieGenreIds.includes(genre.id);
})
.map((genre) => genre.name);
};
In your MovieItem component. (This is were the real problem was)
Instead of:
<div>{movieGenres}</div>
You may want to do something like this:
<div>{movieGenres.join(' ')}</div>
This converts your array into a string that can be rendered. Your error was due to the fact that you were passing there an array of objects that React couldn't render.
If you have any doubt, please let me know.
NOTE: I suggest you to use a type checker to avoid this kind of problems. And to be consistent with your variables naming conventions.
Update based on new information from chat:
In your ExpandableFilters component, you must fix the following piece of code to get the genre name (string). As explained in chat, you can't have objects as a result for a JSX expression ({}), but only primitives that can be coerced to strings, JSX elements or an array of JSX elements.
<GenreFilterCont marginTop>
{filtersShown && (
<ExpandableFiltersUl>
{this.props.movieGenres.map((genre, index) => {
return (
<ExpandableFiltersLi key={index}>
<Checkbox />
{genre.name}
</ExpandableFiltersLi>
);
})}
</ExpandableFiltersUl>
)}
</GenreFilterCont>
Please also note that I've added a key property. You should do it whenever you have a list of elements to render. For more about this I will refer you to the React Docs.
I am trying to build a chat application with the functionality of input field which can be used as filter for chat_groups array which is in the state as chat_groups. Here is how my code looks:
constructor(props) {
super(props);
this.state = {
currUserId: "--id--",
chats: [],
chat_groups: [],
users: [],
};
}
.
.
.
<input
className="chat-group__search__input"
placeholder="Search for group..."
onChange={(ev) => {
console.log(ev.currentTarget.value);
var thatState = this.state;
thatState.chat_groups = thatState.chat_groups.map(
(gp) => {
gp["visible"] = gp.group_name
.toLowerCase()
.includes(ev.currentTarget.value);
return gp;
}
);
// getting correct state in thatState variable
this.setState(thatState);
}}
/>
// getting old state in setState callback and componentDidUpdate lifecycle
The weird problem is I am getting the correct value in thatState variable before setting state. But after setState function is called, if I try to check the state in setState callback or componentDidUpdate lifecycle, I am getting the old state only.
I tried that for keydown and change events also. So, seems to be less of an issue of event as well.
I would like to know if some issue in the code is evident or there is something that I can do to debug the issue.
Edit: After changes, my current onChange looks as below, but the issue is still there; the setState function does not seem to change the state as I can see only the old state in componentDidUpdate lifecycle and setState callback.
onChange={(ev) => {
console.log(ev.currentTarget.value);
let chat_groups = this.state.chat_groups.map((gp) => ({
...gp,
visible: gp.group_name
.toLowerCase()
.includes(ev.currentTarget.value),
}));
console.log(
"Before",
chat_groups.map((gp) => gp.visible)
);
this.setState({ chat_groups: chat_groups });
}}
The problem is that you are mutating the state.
When you do var thatState = this.state; the reference is still the same for both the objects. So automatically when you update thatState.chat_groups you are updating/mutating state as well.
Change your onChange method to like below
onChange = ev => {
console.log(ev.currentTarget.value);
let { chat_groups } = this.state;
chat_groups = chat_groups.map(gp => ({
...gp,
visible: gp.group_name.toLowerCase().includes(ev.currentTarget.value)
}));
this.setState(state => ({
...state,
chat_groups
}));
};
//Other code
//....
//....
<input
className="chat-group__search__input"
placeholder="Search for group..."
onChange={this.onChange} />
I suspect there's one problem while checking the group_name with the input value i.e., you are converting the group_name to lower case using gp.group_name.toLowerCase() but the input value you are not converting to lower case. This could be one issue why the visible attribute value is not getting updated. So in the below snippet I have converted the input value also to lower case while comparing.
Here, below is a runnable snippet with your requirement. Doing console.log in the setState's callback function and the state is getting updated.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
currUserId: "--id--",
chats: [],
chat_groups: [{
group_name: "Group One"
}, {
group_name: "Group Two"
}],
users: []
}
}
onChange = ev => {
console.log(ev.currentTarget.value);
let {
chat_groups
} = this.state;
chat_groups = chat_groups.map(gp => ({
...gp,
visible: gp.group_name.toLowerCase().includes(ev.currentTarget.value.toLowerCase())
}));
this.setState(state => ({
...state,
chat_groups
}), () => { console.log(this.state.chat_groups); });
};
render() {
return <input
placeholder="Search for group..."
onChange={this.onChange} />
}
}
ReactDOM.render(<App />, document.getElementById("react"));
.as-console-wrapper {
max-height: 100% !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>
No, don't do this var thatState = this.state it's just an object it will easily get mutate and you will not get the update as for react state change never occured.
instead do this var { chat_groups } = this.state and then use it further and inlast set the state this.setState({ chat_groups: chat_groups }) also try to avoid the mutation as much as possible means copy the values of chat_groups also
Seems like you are trying to manipulate state directly which is a big no in React.
onChange={(ev) => {
this.setState({
chat_groups: this.state.chat_groups.map((gp) => {
gp["visible"] = gp.group_name
.toLowerCase()
.includes(ev.currentTarget.value);
return gp;
})
});
}}
State should only be updated using the setState method.
You are mutating the state directly in your code above - this isn't recommended. You would get unexpected results and it's not predictable.
This is how you should do it - create a new updated object and pass it to the setState.
onChange={(ev) => {
console.log(ev.currentTarget.value);
const updatedChatGroups = this.state.chat_groups.map((gp) => {
const visible = gp.group_name.toLowerCase().includes(ev.currentTarget.value);
return {
...gp,
visible,
};
});
// Update the modified object using this.setState().
this.setState({ chat_groups: updatedChatGroups });
}}
Read More
I'm trying to update my state. I have declared :
const [s_groupes, setGroupes] = useState(
initialGroupes.map(groupe => {
return Object.assign({
name: groupe,
value: true
})
})
)
From an array, and I wish to update this state when I click on a child component.
The problem is I don't know how to pass the function setGroupes to a child component.
I have no problem using onClick={setGroupes} on a button inside the same file, but I don't understand how to use it with props like this :
<PieChart
data={s_groupes}
disableGroupe={click => disableGroupe(click)}
/>
where my function disableGroupe is :
const disableGroupe = click => {
let value
const groupeClicked = s_groupes.find(groupe => {
value = !groupe.value
return groupe.name === click.data.value.name
})
const newGroupes = s_groupes.map(groupe => {
if (groupe.name === groupeClicked.name) {
return { ...groupe, value }
} else {
return { ...groupe }
}
})
setGroupes(newGroupes)
}
It seems to trigger too many renders but I don't get why. Also from consoles.logs it seems it "reset" my state every render.
I must add I'm using on("click", e => return props.disableGroupe(e)) from the D3JS Library in the child component PieChart , the e returns actually something with data.value.name inside (but I'm not sure it's relevant)
I have a function that takes a Component as its' parameter. The function enables users to render their own popups instead of the ones I provide. However, I'm not able to add some props to said component before adding it to an array.
const addCustomSnack = (Snack, position) => {
let id = generate();
let snackProps = {
key: id,
id,
};
Snack.props = {...Snack.props, ...snackProps}
console.log(Snack);
buildStyle(position);
if (messagesNew.length >= 3) {
que.push(Snack);
addSnacks(messagesNew);
} else {
messagesNew = [...messagesNew, Snack];
addSnacks(messagesNew);
}
console.log(messagesNew);
};
This is what happens
Cannot assign to read only property 'props' of object '#<Object>'
I have tried the following code
const addCustomSnack = (Snack, position) => {
let id = generate();
console.log(Snack);
buildStyle(position);
if (messagesNew.length >= 3) {
que.push(Snack);
addSnacks(messagesNew);
} else {
messagesNew = [...messagesNew, <Snack key={id} id={id} />];
addSnacks(messagesNew);
}
console.log(messagesNew);
};
However, it will result in a React.createElement type error.
Codesandbox
Is there any way for me to add those props into the Snack component successfully?
This is exactly what a react High Order Component does: adding props to the component passed as parameter and return a component back.
If you are getting component in Snack then try below way
return <Snack {...snackProps} />
Using above code this will render any component that is passed to addCustomSnack
You could somehow keep an array of the component to render, each with a ref to the component and and its custom properties, then render it with a map, like so:
// Snack list
constructor(){
this.state = { snacks: [] }
}
// ...
const Lollipop = props => (
<div>
<h1>Lollipop</h1>
<span>Taste: </span> {props.taste}
</div>
)
const ChocolateBar = props => (
<div>
<h1>Chocolate bar</h1>
<span>With fudge: </span> {props.hasFudge ? 'yes': 'no'}
</div>
)
// Push a custom snack in the list
const addCustomSnack = (SnackType, props) => this.state.snacks.push({SnackType, props})
// ...
addSnack(Lollipop, {taste: 'cherry'})
addSnack(Lollipop, {taste: 'cola'})
addSnack(ChocolateBar, {hasFudge: true})
addSnack(ChocolateBar, {})
// render the lsit
const SnackList = () => {
<div>
{ this.state.snacks.map(({SnackType, props}, i) => (
<SnackType {...props} key={i} />
))}
</div>
}
React.cloneElement did exactly what I was looking for. Now, the user can give his own Component, and with cloneElement, I can extend the components props and add it into the array without problems.
const addCustomSnack = (Component, position) => {
let id = generate();
let props = {
removeSnack,
key: id,
id,
index: id
}
let Snack = React.cloneElement(Component, { ...props }, null);
buildStyle(position);
console.log(Snack)
if (messagesNew.length >= 3) {
que.push(Snack);
return addSnacks(messagesNew);
} else {
messagesNew = [...messagesNew, Snack];
return addSnacks(messagesNew);
}
};
import React, {Component} from 'react';
import "./DisplayCard.css";
class DisplayCard extends Component {
runArray = (array) => {
for (var i = 0; i<array.length; i++) {
return <div>{array[i].task}</div>
}
}
renderElements = (savedTasks) =>{
if (savedTasks.length === 0) {
return <div className="noTasks"> <p>You have no saved tasks.</p> </div>
} else {
return this.runArray(savedTasks)
}
}
render() {
return (
<div className="DisplayCardContainer">
{this.renderElements(this.props.saved)}
</div>
)
}
}
export default DisplayCard;
Hey guys,
I am new to react, so this is my child component that takes state from its parent component. My goal is to re-render component every time the array this.props.saved is changed.
This component renders: <p>You have no saved tasks.</p> when the this.props.saved.length === 0 and it renders <div>{array[0].task}</div> when i enter the first task, but it keeps it at <div>{array[0].task}</div> after that. I do see that the state keeps changing and this.props.saved keeps getting bigger, but my component doesn't change anymore.
Here's your problem:
runArray = (array) => {
for (var i = 0; i<array.length; i++) {
//the first time we get here, it immediately ends the function!
return <div>{array[i].task}</div>
}
}
This loop only ever goes through once (at i=0) and then returns, exiting the runArray function and cancelling the rest of the loop. You probably wanted to return an array of elements, one for each of the tasks. I recommend using Array.map() for this, which takes an array and transforms each element, creating a new array:
runArray = (array) => {
return array.map(arrayElement => <div>arrayElement.task</div>);
}
This should do the trick. Note that React may complain about the fact that your elements lack the key property - see the documentation for more info: https://reactjs.org/docs/lists-and-keys.html
The problem is in your runArray function. Inside your loop, you are returning the first element and that's it. My guess is, you see only the first entry?
When you are trying to render all your tasks, I would suggest to map your tasks, e.g.
runArray = (array) => array.map(entry => <div>{entry.task}</div>)
It is because you write wrong the runArray function. You make a return in the for loop so it breaks after the first iteration. It will not iterate over the full array.
You need to transform your for loop to a map : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
runArray = (array) => {
return array.map(v => <div>{v.task}</div>)
}
Does it fix your issue ?
You have to update state of the component to trigger render function. Your render function is not triggered because you did not update the state when the props changed. There are many ways to update state when props updated. One method may be the following:
componentWillReceiveProps(nextProps){
if (nextProps.saved !== this.props.saved) {
this.setState({ saved: nextProps.saved })
}
}
Also change yoour render function to use state of the component as below:
renderElements = () =>{
if (this.state.savedTasks.length === 0) {
return <div className="noTasks"> <p>You have no saved tasks.</p> </div>
} else {
return this.runArray(this.state.savedTasks)
}
}
Use .map so that it renders your task correctly. You can remove runArray and rely entirely on props so you don't need to pass arguments across functions as it can get messy quickly. Here's a quick running example of how to create a parent component where you can add a task and pass them into a component so that it renders your data when props are changed, therefore making it reactive.
class App extends React.Component {
state = {
taskLabel: "",
tasks: [
{
id: 1,
label: "Do something"
},
{
id: 2,
label: "Learn sometihng"
}
]
};
handleInput = evt => {
this.setState({
[evt.target.name]: evt.target.value
});
};
handleSubmit = evt => {
evt.preventDefault();
this.setState(prevState => ({
taskLabel: "",
tasks: [
...prevState.tasks,
{
id: prevState.tasks.length + 1,
label: this.state.taskLabel
}
]
}));
};
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input
name="taskLabel"
type="text"
placeholder="Task label"
value={this.state.taskLabel}
onChange={this.handleInput}
/>
<button>Create task</button>
</form>
<DisplayCard tasks={this.state.tasks} />
</div>
);
}
}
class DisplayCard extends React.Component {
renderTasks = () => {
if (this.props.tasks.length !== 0) {
return this.props.tasks.map(task => (
<div key={task.id}>{task.label}</div>
));
} else {
return <div>No tasks</div>;
}
};
render() {
return <div>{this.renderTasks()}</div>;
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>