0

Recently, I was playing with React and got stuck in - what seems to me, a fairly strange problem.

import React, { useState } from "react";
import Person from "./Person";
import Filter from "./Filter";

function People(props) {
  const [people, setPeople] = useState(props.people);

  function filterPeople(name) {
    let people = people.filter((person) =>
      person.name.toLowerCase().startsWith(name.toLowerCase())
    );
    setPeople(people);
  }

  return (
    <div>
      <Filter filter={filterPeople} />
      {people.map((person, id) => (
        <Person key={id} person={person} />
      ))}
    </div>
  );
}

export default People;

In the code above, I'm trying to set the state people from prop people (in line 6), but it's always setting it to an empty array even though the people prop I'm passing from the parent isn't empty. The more strange thing is, when I console.log(props.people), it shows currect data (which is an array of 3 element)! It would be really helpful if someone could explain what's going on with my implementation.

Mr. Cob
  • 3
  • 3
  • 1
    Your state will only be set to `props.people` the *very first time* your component renders. Every subsequent call the argument is discarded, and this is made very clear in the documentation. – Jared Smith Aug 10 '23 at 17:42
  • You have to refresh the value on subsequent renders using [`useEffect()`](https://legacy.reactjs.org/docs/hooks-effect.html). – M0nst3R Aug 10 '23 at 17:42
  • Does this answer your question? [How to initialize the react functional component state from props](https://stackoverflow.com/questions/59289536/how-to-initialize-the-react-functional-component-state-from-props) – M0nst3R Aug 10 '23 at 17:43
  • 1
    How are you *using* this component and passing the prop? It sounds like you may be duplicating state, trying to store `people` both in this component's state *and* the parent component's state. – David Aug 10 '23 at 17:46
  • @M0nst3R have tried `useEffect()` which gave the same result – Mr. Cob Aug 10 '23 at 18:59
  • @David Yes, I'm duplicating the state - for later filtering functionalities. – Mr. Cob Aug 10 '23 at 19:00
  • @mr cob can you show us how you used `useEffect()` in a [Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example)? – M0nst3R Aug 10 '23 at 19:06
  • @mrcob: If the *intent* is indeed to duplicate state then once it's been duplicated, well, it's been duplicated. If you want to update the state in the `People` component then you'd use `setPeople()`, not change the props which originally initialized it. – David Aug 10 '23 at 19:08

1 Answers1

2

Based on comments on the question, you appear to be duplicating state. That is, the parent component maintains people in state, and this People component also maintains people in state.

Which means that when the props change, this component will still be using its own local state.

Without additional context... don't do that. If the goal is to use whatever is passed in props, just use that:

function People(props) {

  function filterPeople(name) {
    const filteredPeople = people.filter((person) =>
      person.name.toLowerCase().startsWith(name.toLowerCase())
    );
    props.setPeople(filteredPeople);
  }

  return (
    <div>
      <Filter filter={filterPeople} />
      {people.map((person, id) => (
        <Person key={id} person={person} />
      ))}
    </div>
  );
}

This would require not only passing people as a prop, but also setPeople.


Alternatively: Still without context, but maybe you don't want to actually update the parent component's state but instead want to only locally "filter" the people array in this component. In that case you could track that name value in state and filter directly in the rendering. For example:

function People(props) {
  const [name, setName] = useState('');

  const filteredPeople = people.filter((person) =>
    person.name.toLowerCase().startsWith(name.toLowerCase())
  );

  return (
    <div>
      <Filter filter={setName} />
      {filteredPeople.map((person, id) => (
        <Person key={id} person={person} />
      ))}
    </div>
  );
}

In most cases this won't incur a significant enough performance penalty to worry about, but if it does then you can also memo-ize filteredPeople:

function People(props) {
  const [name, setName] = useState('');

  const filterPeople = useMemo(() => {
    return people.filter((person) =>
      person.name.toLowerCase().startsWith(name.toLowerCase())
    );
  }, [people, name]);

  const filteredPeople = filterPeople();

  return (
    <div>
      <Filter filter={setName} />
      {filteredPeople.map((person, id) => (
        <Person key={id} person={person} />
      ))}
    </div>
  );
}

Alternatively: You could keep state duplicated if you plan to perform further operations at a later time on a localized copy of the original state without modifying the parent state. In that case if you want to update the local state any time the prop changes, you can do that with useEffect:

function People(props) {
  const [people, setPeople] = useState(props.people);

  useEffect(() => {
    setPeople(props.people);
  }, [props.people]);

  function filterPeople(name) {
    let people = people.filter((person) =>
      person.name.toLowerCase().startsWith(name.toLowerCase())
    );
    setPeople(people);
  }

  return (
    <div>
      <Filter filter={filterPeople} />
      {people.map((person, id) => (
        <Person key={id} person={person} />
      ))}
    </div>
  );
}

This would effectively over-write whatever changes have been made to local state any time the parent component sends a different value for props.people.

David
  • 208,112
  • 36
  • 198
  • 279