Search code examples
javascriptreactjsreact-hookssetstatereact-table

Child component's local state changes when change happens inside parent component : ReactJS


I have a situation where I am implementing custom dropdown filter for a table in react. I have set of dropdown values for each column and there is a Apply button.

I have maintained a child component for this which takes in drop down values and sends the selected one's back to parent.

The filtering happens but when I open this dropdown again the checkbox values gets lost.

Can someone tell where I am going wrong?

I am stuck here

Sand box: https://codesandbox.io/s/nervous-elgamal-0zztb

I have added the sandbox link with proper comments. Please have a look. I am a bit new to react.

Help would be really appreciated

Parent

import * as React from "react";
import { render } from "react-dom";
import ReactTable from "react-table";
import "./styles.css";
import "react-table/react-table.css";
import Child from "./Child";
interface IState {
  data: {}[];
  columns: {}[];
  selectedValues: {};
  optionsForColumns: {};
}

interface IProps {}

export default class App extends React.Component<IProps, IState> {

  // Here I have  hardcoded the values, but data and optionsForColumns comes from the backend and it is set inside componentDidMount
  constructor(props: any) {
    super(props);
    this.state = {
      data: [
        { firstName: "Jack", status: "Submitted", age: "14" },
        { firstName: "Simon", status: "Pending", age: "15" }
      ],
      selectedValues: {},
      columns: [],
      optionsForColumns: {
        firstName: [{ Jack: "4" }, { Simon: "5" }],
        status: [{ Submitted: "5" }, { Pending: "7" }]
      }
    };
  }

  // Get the values for checkboxes that will be sent to child
  getValuesFromKey = (key: any) => {
    let data: any = this.state.optionsForColumns[key];
    let result = data.map((value: any) => {
      let keys = Object.keys(value);
      return {
        field: keys[0],
        checked: false
      };
    });
    return result;
  };

  // Get the consolidated values from child and then pass it for server side filtering
  handleFilter = (fieldName: any, selectedValue: any, modifiedObj: any) => 
  {
    this.setState(
      {
        selectedValues: {
          ...this.state.selectedValues,
          [fieldName]: selectedValue
        }
      },
      () => this.handleColumnFilter(this.state.selectedValues)
    );
  };

  // Function that will make server call based on the checked values from child
  handleColumnFilter = (values: any) => {
    // server side code for filtering
    // After this checkbox content is lost
  };

  // Function where I configure the columns array for the table . (Also data and column fiter values will be set here, in this case I have hardcoded inside constructor)
  componentDidMount() {
    let columns = [
      {
        Header: () => (
          <div>
            <div>
              <Child
                key="firstName"
                name="firstName"
                options={this.getValuesFromKey("firstName")}
                handleFilter={this.handleFilter}
              />
            </div>
            <span>First Name</span>
          </div>
        ),
        accessor: "firstName"
      },
      {
        Header: () => (
          <div>
            <div>
              <Child
                key="status"
                name="status"
                options={this.getValuesFromKey("status")}
                handleFilter={this.handleFilter}
              />
            </div>
            <span>Status</span>
          </div>
        ),
        accessor: "status",
      },
      {
        Header: "Age",
        accessor: "age"
      }
    ];
    this.setState({ columns });
  }

  //Rendering the data table
  render() {
    const { data, columns } = this.state;
    return (
      <div>
        <ReactTable
          data={data}
          columns={columns}
        />
      </div>
    );
  }
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);

Child


import * as React from "react";
import { Button, Checkbox, Icon } from "semantic-ui-react";
interface IProps {
  options: any;
  name: string;
  handleFilter(val1: any, val2: any, val3: void): void;
}
interface IState {
  showList: boolean;
  selected: [];
  checkboxOptions: any;
}
export default class Child extends React.Component<IProps, IState> {
  constructor(props: any) {
    super(props);
    this.state = {
      selected: [],
      showList: false,
      checkboxOptions: this.props.options.map((option: any) => option.checked)
    };
  }

  // Checkbox change handler
  handleValueChange = (event: React.FormEvent<HTMLInputElement>, data: any) => {
    const i = this.props.options.findIndex(
      (item: any) => item.field === data.name
    );
    const optionsArr = this.state.checkboxOptions.map(
      (prevState: any, si: any) => (si === i ? !prevState : prevState)
    );
    this.setState({ checkboxOptions: optionsArr });
  };

  //Passing the checked values back to parent
  passSelectionToParent = (event: any) => {
    event.preventDefault();
    const result = this.props.options.map((item: any, i: any) =>
      Object.assign({}, item, {
        checked: this.state.checkboxOptions[i]
      })
    );
    const selected = result
      .filter((res: any) => res.checked)
      .map((ele: any) => ele.field);
    console.log(selected);
    this.props.handleFilter(this.props.name, selected, result);
  };

  //Show/Hide filter
  toggleList = () => {
    this.setState(prevState => ({ showList: !prevState.showList }));
  };

  //Rendering the checkboxes based on the local state, but still it gets lost after filtering happens
  render() {
    let { showList } = this.state;
    let visibleFlag: string;
    if (showList === true) visibleFlag = "visible";
    else visibleFlag = "";
    return (
      <div>
        <div style={{ position: "absolute" }}>
          <div
            className={"ui scrolling dropdown column-settings " + visibleFlag}
          >
            <Icon className="filter" onClick={this.toggleList} />
            <div className={"menu transition " + visibleFlag}>
              <div className="menu-item-holder">
                {this.props.options.map((item: any, i: number) => (
                  <div className="menu-item" key={i}>
                    <Checkbox
                      name={item.field}
                      onChange={this.handleValueChange}
                      label={item.field}
                      checked={this.state.checkboxOptions[i]}
                    />
                  </div>
                ))}
              </div>
              <div className="menu-btn-holder">
                <Button size="small" onClick={this.passSelectionToParent}>
                  Apply
                </Button>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }
}





Solution

  • Sorry, my here below answer was wrong.

    In your main component, you set your state.columns just once, when the component did mount. It will never be changed after.

    But each time your state.data get changed, your main component get re-rendered and the ReactTable component get re-rendered as well with the new data AND with your original state.columns which get no info about the selected options.

    I think you should reconstruct your column object after each update, passing in your Child components a prop like selectedOption.

    Your child props depend on your state.selectedValues and state.data. You can reconstruct your column object in your render(). I'm using a method buildColumns to get the details out of render.

    You don't need the columnsproperty in your state.

    ...
    constructor(props) {
        super(props);
        this.state = {
          data: [
            { firstName: "Jack", status: "Submitted", age: "14" },
            { firstName: "Simon", status: "Pending", age: "15" },
            { firstName: "Pete", status: "Approved", age: "16" },
            { firstName: "Lucas", status: "Rejected", age: "19" }
          ],
          selectedValues: {},
        };
      }
    ...
    buildColumns = ({ data, selectedValues = {} }) => {
        const { firstName, status } = selectedValues
        return [
          {
            Header: () => (
              <div>
                <div style={{ position: "absolute", marginLeft: "10px" }}>
                  <Child
                    key="firstName"
                    name="firstName"
                    options={this.getValuesFromKey(data, "firstName")}
                    selectedOption={firstName}
                    handleFilter={this.handleFilter}
                  />
                </div>
                <span>First Name</span>
              </div>
            ),
            accessor: "firstName",
            sortable: false,
            show: true,
            displayValue: " First Name"
          },
          {
            Header: () => (
              <div>
                <div style={{ position: "absolute", marginLeft: "10px" }}>
                  <Child
                    key="status"
                    name="status"
                    options={this.getValuesFromKey(data, "status")}
                    selectedOption={status}
                    handleFilter={this.handleFilter}
                  />
                </div>
                <span>Status</span>
              </div>
            ),
            accessor: "status",
            sortable: false
          },
          {
            Header: "Age",
            accessor: "age"
          }
        ];
      }
    
      render() {
        const { data, selectedValues } = this.state;
        const columns = this.buildColumns({ data, selectedValues })
        return (
          <div>
            <ReactTable
              data={data}
              columns={columns}
              defaultPageSize={10}
              className="-striped -highlight"
            />
          </div>
        );
      }
    

    You still have to use the selectedOption props inside your Child component to precheck your checkbox.


    old (and wrong) answer:

    When you get filtered data and set your state with it, now that your state.data has changed, it will re-render the Child component which get props derived from it.

    This will reinitialize the state of your Child component to its {selected: []}value.

    To prevent that, you should make your Child component a 'controlled component' by keeping your state only in your Main component, and passing the info of the selected option by props: https://reactjs.org/docs/forms.html#controlled-components