1

This is the code within my react component

const mockFetch = () => Promise.resolve({
  json: () => new Promise((resolve) => setTimeout(() => resolve({
    student1: {
      studentName: 'student1'
    },
    student2: {
      studentName: 'student2'
    },
    student3: {
      studentName: 'student3'
    }
  }), 250))
})

function MyApp() {
  const [result, setResult] = React.useState([]);
  const [queryString, setQueryString] = React.useState("");

  function searchHandler() {
    mockFetch(
        "{url that i defined in real code}"
      )
      .then(response => response.json())
      .then(data => {
        Object.keys(data).filter(element => {
          if (queryString === "") {
            setResult(result.push(data[element].studentName));
          } else if (data[element].studentName.toLowerCase().includes(queryString.toLowerCase())) {
            setResult(result.push(data[element].studentName));
          }
          setResult(result.join("<br/>"));
        })
      });

  }

  return (
    <div>
      <div>
        <input type = "text" placeholder = "Search..."
          onChange={event => setQueryString(event.target.value)} />
        <button onClick={searchHandler}>Search</button>
      </div>
      <output>
        Result:
        <br /><br />{result}<br /><br /><br /> <br />
        Query String:
        <br /><br />{queryString}<br /><br />
      </output>
    </div>
  );
}

ReactDOM.render(<MyApp />, document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

When I am trying to search(filter) students from api response by studentName on the second click to search it breaks with result.push() type error.

The behavior I want to achieve:

  1. Upon clicking the search button I want to filter api data by studentName
  2. If queryString is empty I want to show all the items unfiltered
  3. I want to display the results with <br/> tag in between
Daedalus
  • 7,586
  • 5
  • 36
  • 61
Airlo
  • 91
  • 2
  • 9
  • so you are using filter but don't return anything from filter why are you using filter? What are you trying to achieve? – Karen Grigoryan Oct 20 '21 at 20:06
  • I am getting a database's data and each object in the data has a property called studentName. I am trying to filter out all the ones that have a studentName property that does not include queryString that is if queryString is not an empty string and then add all the values of the studentName properties that do include queryString add to the result array. If queryString is an empty string then I will just return all objects. – Airlo Oct 20 '21 at 20:10
  • why do you do join(
    ) in each filter iteration?
    – Karen Grigoryan Oct 20 '21 at 20:12
  • That was just a test to see if I could include a breakline but that has nothing to do with the problem right now. – Airlo Oct 20 '21 at 20:12
  • Ok so what you are trying to achieve is whenever you click search, you want to show a list of results matching the query by studentname. And if it's an empty string show all. That's it right? – Karen Grigoryan Oct 20 '21 at 20:15
  • Yes exactly. Store the results in array. The problem I am having is trying to add the studentName value to the result array as I get a TypeError. – Airlo Oct 20 '21 at 20:17
  • I am on it. Will post the answer in a bit. – Karen Grigoryan Oct 20 '21 at 20:20
  • Thank you very much I appreciate it alot. – Airlo Oct 20 '21 at 20:23
  • 1
    Please add the code into your question. – Spectric Oct 20 '21 at 20:25
  • I did. The code is in the hyperlink. – Airlo Oct 20 '21 at 20:30
  • Please read [ask], where it says, "*If it is possible to create a live example of the problem that you can link to (for example, on http://sqlfiddle.com/ or http://jsbin.com/) then do so - **but also copy the code into the question itself.***" When your pastebin is deleted, this question will no longer make sense. Stack Overflow exists to help you *and everyone else with the same question as you*, with more importance on the latter. – Heretic Monkey Oct 20 '21 at 20:47
  • @Airlo please have a look – Karen Grigoryan Oct 20 '21 at 21:04
  • Hey @Airlo I submitted an edit to the question for you so that you can gain reputation for your question and I can hopefully continue to gain reputation for my answer to your question ;) Feel free to edit if I miss something – Karen Grigoryan Oct 21 '21 at 14:38
  • @KarenGrigoryan Please do not add a thank you into the post, and see [why you should refrain from doing this](https://meta.stackoverflow.com/questions/328379/why-are-fellow-users-removing-thank-yous-from-my-questions). – Daedalus Oct 31 '21 at 07:46
  • @Daedalus can you be more specific about "thank you" because I cannot find any mentions of "thank you" in my post. – Karen Grigoryan Oct 31 '21 at 07:59
  • @KarenGrigoryan Because I removed it, but you quite clearly added it in [here](https://stackoverflow.com/revisions/69652159/3), on revision 3 to this question. – Daedalus Oct 31 '21 at 08:03
  • @Daedalus ah ok you meant my edit of the question not my answer. Noted thanks. – Karen Grigoryan Oct 31 '21 at 08:03

2 Answers2

2

You should be spreading your new value into your state setter. That is to say:

setResult(result.push(data[element].studentName));

should be

setResult([...result, data[element].studentName]);

Also I expect you intended to use element instead of data[element] in your filter block. Although your filter is logic is also incorrect. your callback function is meant to return a boolean result not set state. Perhaps you meant to do Object.keys(data).forEach(...

Damian Green
  • 6,895
  • 2
  • 31
  • 43
  • I switched out that code for my code but it is not what my goal is. I am trying to get the database and check if the queryString is included in any of the studentName properties of the database. The only part I am struggling on is making the result an array for all of the studentName properties that include the string. With your code I was only able to make an array that was set the last studentName property that is iterated through. Unless there is something else wrong with my code. – Airlo Oct 20 '21 at 19:56
  • that sounds like another issue – Damian Green Oct 20 '21 at 19:58
  • What do you think it could be? – Airlo Oct 20 '21 at 19:59
  • this won't work because `result` within searchHandler is a reference to the same closure variable so even though within promise handlers react doesn't batch setstate calls and triggers separate setState actions those will still rewrite each other and apply only the latest one hence what @Airlo is seeing. The correct way of fixing this in your example is to use a closure within setState to concat with previous value result without overwriting it like this `setResult((prevResult) => prevResult.concat(data[element].studentName))` – Karen Grigoryan Oct 20 '21 at 21:28
  • I added a better and more visual explanation of my previous comment to my answer above. Happy to hear feedback – Karen Grigoryan Oct 21 '21 at 14:07
2

Let's go through a couple of observations on why original code doesn't work as intended

Mispurposed filtering

In original SearchHandler you have this piece

(data => {
  Object.keys(data).filter(element => {
    if (queryString === "") {
      setResult(result.push(data[element].studentName));
    } else if (data[element].studentName.toLowerCase().includes(queryString.toLowerCase())) {
      setResult(result.push(data[element].studentName));
    }
    setResult(result.join("<br/>"));
  })
});

So there Object.keys(data) gets all the keys of data and then filter runs through every single key and returns nothing.

filter callback is supposed to return a boolean so that filter method can decide which item of the array to store and which not to.

And then ideally you are supposed to do something with the returned filtered array ideally which is not happening here.

In the end of this post I will suggest what exactly.

Setting state with the wrong result type

Next let's focus on this portion

  setResult(result.push(data[element].studentName));

Here you have 2 things happening:

  • result.push(data[element].studentName) that returns the number of elements of a new array after push. Let's for the sake of example say it's 5
  • And then you call setResult with that 5 i.e. setResult(5)

I think this is not what you intended to persist in the result state. But it gets even deeper.

Note on setState scheduling

Let's pivot a little into how react schedules setState calls to be able lay ground for our further explanations

For a more in-depth answer on that please check the details here https://stackoverflow.com/a/48610973/2998898

But in essence, at the time of this writing, React (v17) currently batches multiple sibling setState calls happening in the same asynchronous callback only if they are called directly in event handler body (like onClick, onChange, ...etc) or directly in the body of hooks (like useEffect).

To visualize the concept have a look at the console in this example

function App() {
  const renderNumberRef = React.useRef(1);
  React.useEffect(() => {
    renderNumberRef.current += 1;
  });

  const [result, setResult] = React.useState([]);

  function searchHandler() {
    Promise.resolve().then(() => {
      console.log("enter searchHandler() promise callback");
      console.log("setResult() with student1");
      setResult([...result, "student1"]);
      console.log("setResult() with student2");
      setResult([...result, "student2"]);
      console.log("setResult() with student3");
      setResult([...result, "student3"]);
      console.log("exit searchHandler() promise callback");
    });
  }

  console.log(
    "Active render number: ",
    renderNumberRef.current,
    "state.result: ",
    result
  );

  return (
    <div>
      <div>
        <button onClick={searchHandler}>
          <div>Search Icon</div>
        </button>
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

In your case setResult() is called inside of searchHandler which is indeed an event handler, BUT it's not in its direct body, it's in fact in a nested fetch().then() promise callback.

That's why instead of batching react will immediately rerender the component on every setResult() call, which will save the value 5 into state over and over and over again until the end of searchHandler promise callback

Fixed reference

Now you might ask why 5 though?

Aren't we supposed to now break on setResult(5.push()) on the very next setResult() call?

We would if it weren't for to the fact that we use a closure reference to the same result variable inside of searchHandler so what you actually do is you keep setResult()-ing the same initial empty array over and over again until the searchHandler function is finished executing.

And every time rerender happens during execution of searchHandler we keep reassigning the same value 5 into state

The Error

And the very next time you click search button again you now take result which now contains 5

And the logic immediately breaks because 5.push() is a Type Error

Potential solution

So as you can see there are many things going wrong in the same place at the same time.

So first you need to make sure you use array filtering with its' primary purpose of returning a filtered array.

You take that array and you save it into the state once after filtering is finished.

Calling setResult multiple times within a filter or while your event is still going is suboptimal and doesn't serve your purpose.

You should always try to mitigate amount of unnecessary rerenders.

The simple mental model that should help is this:

  1. fetch some data
  2. transform/filter the data
  3. set the final result into state
  4. use the result from state to render something on the screen

So I created a rewritten and mocked version of your initial code with explanations within inline comments.

const mockFetch = () =>
  Promise.resolve({
    json: () =>
      new Promise((resolve) => setTimeout(() => resolve({
        element1: { studentName: "student1" },
        element2: { studentName: "student2" },
        element3: { studentName: "student3" }
        // imitating 250ms delay with mocked fetch here
      }), 250))
  });

function App() {
  const [result, setResult] = React.useState([]);
  const [queryString, setQueryString] = React.useState("");

  function searchHandler(capturedQueryString) {
    mockFetch("{url that i defined in real code}")
      .then((response) => response.json())
      .then((data) => {
        // for each search you do not need to run setResult on
        // each iteration like in your initial code above in filter method
        // you just need to calculate once your whole state per request and then
        // set the result in state
        if (capturedQueryString === "") {
          // pay attention here we don't setResult(element.push())
          // because array.push() returns an integer that
          // signifies the length of the new array
          // so in your code above you were persisting the number itself
          // and then calling push() method of a number which fails
          // so instead we persist the array itself
          setResult(
            Object.keys(data).map((element) => data[element].studentName)
          );
        } else {
          setResult(
            Object.keys(data)
              .filter((element) => {
                return data[element].studentName
                  .toLowerCase()
                  .includes(capturedQueryString.toLowerCase());
              })
              .map((filteredElement) => data[filteredElement].studentName)
          );
        }
      });
  }

  return (
    <div>
      <div>
        <input
          type="text"
          id="search-bar"
          placeholder="Search..."
          onChange={(event) => setQueryString(event.target.value)}
        />

        {/* passing in captured queryString
        // so that when clicked within searchHandler
        // we always operate with the same queryString
        // regardless if queryString has changed in state or not
        */}
        <button onClick={() => searchHandler(queryString)}>
          <div>Search Icon</div>
        </button>
      </div>

      <output>
        Result:
        <br />
        <br />
        {/* In order to join your array with <br /> tag you need to inner html it */} 
        {/* React provides that possibility via dangerouslySetInnerHTML api */}
        {/* BUT since your student name can be hijacked by attacker given that your api is also not sanitized */} 
        {/* the best practice is to sanitize the joined string with something like sanitize-html or DOMPurify package */}
        <span dangerouslySetInnerHTML={{__html: result.join('<br />')}} />
        <br />
        <br />
        <br />
        <br />
        Query String:
        <br />
        <br />
        {queryString}
        <br />
        <br />
      </output>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id="root"></div>

P.S. Of course this is not a finished version of the functionality, since it doesn't cover plethora of edge cases like (what happens if you click 3 times and 2nd request resolves after 3rd, or what if one of the requests gets rejected etc etc) but this should be a fine start and should hopefully unblock you.

Karen Grigoryan
  • 5,234
  • 2
  • 21
  • 35
  • Sorry for the late response. I was away for a bit. I ran the code and it worked completely fine though I do not fully understand the part of the comment that says: "you just need to calculate once your whole state per request and then set the result in state". I would appreciate it if you could elaborate or make it easier for me to understand what you are trying to say. – Airlo Oct 20 '21 at 21:23
  • hey @Airlo I will have a look tomorrow and see how to explain better cause it's sleep time here. Cheers. – Karen Grigoryan Oct 20 '21 at 21:30
  • hey @Airlo it's done please check and tell me if this is better or more confusing? – Karen Grigoryan Oct 21 '21 at 13:08
  • 1
    Thank you. I really understand the reason for what was wrong in the code now. I really appreciate you going this far just to help me understand even though you did not have to. – Airlo Oct 21 '21 at 17:24
  • @Airlo I am glad it helped to clarify. Feel free to upvote the answer if you find it useful. Thanks much. – Karen Grigoryan Oct 21 '21 at 17:26
  • Sorry I do not have 15 reputation yet so I cannot upvote the answer. I will make sure to come back once I get 15 reputation and upvote the answer. – Airlo Oct 21 '21 at 18:16