Search code examples
javascriptreactjsreact-tablereact-statereact-state-management

How to change the value of a prop of Sub Component passed as a column to React Table?


I am using React Table Library in Web Application.

I am passing a component as a column to the table. Columns and data for the table looks like below.

  columns = [
    {
      Header: "Name",
      accessor: "name"
    },
    {
      Header: "Action",
      accessor: "action"
    }
  ];

  sampleData = [
    {
      id: 1,
      name: "Product one",
      action: (
        <TestComponent
          id={1}
          inProgress={false}
          onClickHandler={id => this.onClickHandler(id)}
        />
      )
    },
    {
      id: 2,
      name: "Product two",
      action: (
        <TestComponent
          id={2}
          inProgress={false}
          onClickHandler={id => this.onClickHandler(id)}
        />
      )
    }
  ];

My TestComponent Looks like below.

const TestComponent = ({ id, inProgress, onClickHandler }) => {
  return (
    <div>
      {inProgress ? (
        <p>In Progress </p>
      ) : (
        <button onClick={() => onClickHandler(id)}>click me</button>
      )}
    </div>
  );
};

What my purpose is, When user click on click me button it needs to call a Backend API. At that time inProgress prop need to be true and it should pass to the table and need text as In Progress until API call completed.

I could be able to do it changing state as below.

  onClickHandler(id) {
    const newData = this.sampleData.map(data => {
      if (data.id === id) {
        return {
          ...data,
          action: (
            <TestComponent
              id={1}
              inProgress={true}
              onClickHandler={id => this.onClickHandler(id)}
            />
          )
        };
      }
      return data;
    });
    this.setState({ data: newData });

    // Here I Call the Backend API and after complete that need to show the click me button again.
    setTimeout(() => {
      this.setState({ data: this.sampleData });
    }, 3000);
  }

I could be able to achieve what I need But I am not satisfied the way I am changing the state. I need to know is there a better way of doing this without changing state like this.

You can use this StackBlitz Link for giving me a better solution. Thanks.


Solution

  • Instead of passing inProgress prop to TestComponent, you could maintain a local state in the TestComponent that is used to determine whether to show progress text or a button and only pass the id and onClickHanlder props to TestComponent.

    When button is clicked in TestComponent, you could set the the local state of TestComponent to show the progress text and then call the onClickHandler function passed as prop, passing in the id prop and a callback function as arguments. This callback function will be called when API request is completed. This callback function is defined inside TestComponent and only toggles the local state of the TestComponent to hide the progress text and show the button again.

    Change your TestComponent to as shown below:

    const TestComponent = ({ id, onClickHandler }) => {
      const [showProgress, setShowProgress] = React.useState(false);
    
      const toggleShowProgress = () => {
        setShowProgress(showProgress => !showProgress);
      };
    
      const handleClick = () => {
        setShowProgress(true);
        onClickHandler(id, toggleShowProgress);
      };
    
      return (
        <div>
          {showProgress ? (
            <p>In Progress </p>
          ) : (
            <button onClick={handleClick}>click me</button>
          )}
        </div>
      );
    };
    

    i have used useState hook to maintain local state of the TestComponent as it is a functional component but you could use the same logic in a class component as well.

    Change the TestComponent in sampleData array to only be passed two props, id and onClickHandler.

    {
       id: 1,
       name: "Product one",
       action: <TestComponent id={1} onClickHandler={this.onClickHandler} />
     }
    

    and change onClickHandler method in App component to:

    onClickHandler(id, callback) {
       // make the api request, call 'callback' function when request is completed
        setTimeout(() => {
          callback();
        }, 3000);
    }
    

    Demo

    Edit crazy-fire-fvlhm

    Alternatively, you could make onClickHandler function in App component to return a Promise that is fulfilled when API request completes. This way you don't have to pass a callback function from TestComponent to onClickHandler method in App component.

    Change onClickHandler method to:

    onClickHandler(id) {
       return new Promise((resolve, reject) => {
         setTimeout(resolve, 3000);
       });
    }
    

    and change TestComponent to:

    const TestComponent = ({ id, onClickHandler }) => {
      const [showProgress, setShowProgress] = useState(false);
    
      const toggleShowProgress = () => {
        setShowProgress(showProgress => !showProgress);
      };
    
      const handleClick = () => {
        setShowProgress(true);
    
        onClickHandler(id)
          .then(toggleShowProgress)
          .catch(error => {
            toggleShowProgress();
            // handle the error
          });
      };
    
      return (
        <div>
          {showProgress ? (
            <p>In Progress </p>
          ) : (
            <button onClick={handleClick}>click me</button>
          )}
        </div>
      );
    };
    

    Demo

    Edit practical-sinoussi-r92qj