Skip to content
Advertisement

Array of Functional Components doesn’t append props content as expected

I want to add an array of <TokenFeed/> functional components to the <Feeds/> component. The problem is that when I do, the onClick() event that passes some text to my <App/> doesn’t work as expected. The array version of <TokenFeed/> when clicked, will replace my <input/> text rather than appending to the end of it.

On the contrary, when I add a copy of <TokenFeed/> in the return of <Feeds/>, and click the button, it works fine.

How can I fix this?

Demo

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

export default function App() {
  const [inputValue, setInputValue] = useState("");
  const [showFeeds, setShowFeeds] = useState();

  function createFeeds(e) {
    if (e._reactName === "onClick" && showFeeds === undefined) {
      setShowFeeds(
        <Feeds
          value={(val) => {
            setInputValue(inputValue + val);
            createFeeds("");
          }}
        />
      );
    } else {
      setShowFeeds(undefined);
    }
  }
  return (
    <>
      <input
        type="text"
        placeholder="Message"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      ></input>
      <button onClick={(e) => createFeeds(e)}>Create Feeds</button>
      {showFeeds}
    </>
  );
}
import React from "react";
import { TokenFeed } from "./TokenFeed";

let tokenFeedArr = [];

export const Feeds = (props) => {
  if (tokenFeedArr.length === 0) {
    tokenFeedArr.push(
      <TokenFeed
        key={"1"}
        onClick={() => props.value("Array")}
        tokenName={"Array"}
        tokenPrice={"Test"}
      />
    );
  }

  return (
    <section>
      {/* This doesn't work */}
      {tokenFeedArr}

      {/* This does work */}
      <TokenFeed
        key={"2"}
        onClick={() => props.value("Direct")}
        tokenName={"Direct"}
        tokenPrice={"Test"}
      />
    </section>
  );
};
import React from "react";

export const TokenFeed = (props) => {
  return (
    <section
      onClick={() => props.onClick()}
      style={{ backgroundColor: "yellow", width: "10%", textAlign: "center" }}
    >
      <h1>{props.tokenName}</h1>
      <p>{props.tokenPrice}</p>
    </section>
  );
};

Advertisement

Answer

You need to declare let tokenFeedArr = []; inside the Feeds component.

Instead of:

let tokenFeedArr = [];

export const Feeds = (props) => {
  if (tokenFeedArr.length === 0) {
    tokenFeedArr.push(
      <TokenFeed
        key={"1"}
        onClick={() => props.value("Array")}
        tokenName={"Array"}
        tokenPrice={"Test"}
      />
    );
  }
...

Try this:

export const Feeds = (props) => {
  const tokenFeedArr = [];
  if (tokenFeedArr.length === 0) {
    tokenFeedArr.push(
      <TokenFeed
        key={"1"}
        onClick={() => props.value("Array")}
        tokenName={"Array"}
        tokenPrice={"Test"}
      />
    );
  }
...

The reason the tokenFeedArr that is declared outside the Feeds component doesn’t work has to do with JavaScript closures. Specifically the issue lies within the value() function that’s inside instances of TokenFeed inside the tokenFeedArr.

The value() function that is passed to an instance of TokenFeed inside of the tokenFeedArr only has access to inputValue as it was when the component was mounted (which is an empty string). It’s not connected to inputValue via React state, because it’s outside of the scope of the exported component. This is true even though the TokenFeed components are pushed to tokenFeedArr inside the Feeds component. tokenFeedArr is still declared outside Feeds. The setInputValue function works, because of how the useState hook works, but the inputValue variable is just a variable and is subject to JavaScript closures/hoisting, which causes it to retain its original value.

Advertisement