Как вызвать метод дочернего компонента из родительского - PullRequest
1 голос
/ 01 октября 2019

В моем приложении Reactjs мне нужно иметь родительский компонент (мастер) с именем Wizard.js и несколько дочерних компонентов (шаги мастера) с именем PrimaryForm.js , SecondaryForm.js и т. Д. Все они являются компонентами на основе классов с некоторыми локальными функциями проверки.

Кнопки «Назад» и «Далее» для продвижения по шагам находятся в Wizard.js.

Чтобы перейти к следующему шагу мастера, я пытаюсь вызвать метод из PrimaryForm. Я проверял подобные вопросы в Stackoverflow;попытался использовать ref или forwardRef, но я не мог заставить его работать. В настоящее время я получаю сообщение " TypeError: Невозможно прочитать свойство handleCheckServer 'с нулевым значением ".

Ниже приведены мои родительские и дочерние классы. Любая помощь о том, что я буду делать неправильно, приветствуется.

Wizard.js:

import React, { Component } from 'react';
...

const getSteps = () => {
  return [
    'Info',
    'Source Details',
    'Target Details',
    'Configuration'
  ];
}

class Wizard extends Component {
  constructor(props) {
    super(props);
    this.firstRef = React.createRef();
    this.handleNext = this.handleNext.bind(this);

    this.state = {
      activeStep: 1,
    }
}

  componentDidMount() {}

  handleNext = () =>  {
    if (this.state.activeStep === 1) {
      this.firstRef.current.handleCheckServer(); <<<<<<<<<<<<<<<<< This is where I try to call child method
    }
    this.setState(state => ({
      activeStep: state.activeStep + 1,
    }));
  };

  handleBack = () => {
    this.setState(state => ({
      activeStep: state.activeStep - 1,
    }));
  };

  handleReset = () => {
    this.setState({
      activeStep: 0,
    });
  };

  render() {
    const steps = getSteps();
    const currentPath = this.props.location.pathname;
    const { classes } = this.props;

    return (
      <React.Fragment>
        <CssBaseline />
        <Topbar currentPath={currentPath} />
        <div className={classes.root}>
          <Grid container spacing={2} justify="center" direction="row">
            <Grid container spacing={2} className={classes.grid} justify="center" direction="row">
              <Grid item xs={12}>
                <div className={classes.topBar}>
                  <div className={classes.block}>
                    <Typography variant="h6" gutterBottom>Wizard</Typography>
                    <Typography variant="body1">Follow the wizard steps to create a configuration.</Typography>
                  </div>
                </div>
              </Grid>
            </Grid>
            <Grid container spacing={2} alignItems="center" justify="center" className={classes.grid}>
              <Grid item xs={12}>
                <div className={classes.stepContainer}>
                  <div className={classes.bigContainer}>
                    <Stepper classes={{ root: classes.stepper }} activeStep={this.state.activeStep} alternativeLabel>
                      {steps.map(label => {
                        return (
                          <Step key={label}>
                            <StepLabel>{label}</StepLabel>
                          </Step>
                        );
                      })}
                    </Stepper>
                  </div>
                  <PrimaryForm ref={this.firstRef} />
                </div>
              </Grid>
            </Grid>
            <Grid container spacing={2} className={classes.grid}>
            <Grid item xs={12}>
              <div className={classes.flexBar}>
                <Tooltip title="Back to previous step">
                  <div>
                    <Button variant="contained"
                      disabled={(this.state.activeStep === 0)}
                      className={classes.actionButton}
                      onClick={this.handleBack}
                      size='large'>
                      <BackIcon className={classes.rightIcon} />Back
                      </Button>
                  </div>
                </Tooltip>
                <Tooltip title="Proceed the next step">
                  <div>
                    <Button
                      variant="contained" className={classes.actionButton}
                      color="primary"
                      size='large'
                      disabled={!(!this.state.isFormValid || this.state.isTestWaiting)}
                      onClick={this.handleNext}>
                    <ForwardIcon className={this.props.classes.rightIcon}/>Next</Button>
                  </div>
                </Tooltip>

                <Tooltip title="Cancel creating new configuration">
                  <Button variant="contained" color="default" className={classes.actionButton}
                    component={Link} to={'/configs'} style={{ marginLeft: 'auto' }}>
                    <CancelIcon className={classes.rightIcon} />Cancel
                      </Button>
                </Tooltip>
              </div>
            </Grid>
          </Grid>
          </Grid>
        </div>
      </React.Fragment>
    )
  }
}
export default withRouter(withStyles(styles)(Wizard));

PrimaryForm.js:

import React, { Component } from 'react';
...

class PrimaryForm extends Component {

  constructor(props) {
    super(props);
    this.handleCheckServer = this.handleCheckServer.bind(this);

    this.state = {
      hostname: {
        value: "localhost",
        isError: false,
        errorText: "",
      },
      serverIp: {
        value: "127.0.0.1",
        isError: false,
        errorText: "",
      },
      isFormValid: true,
      isTestValid: true,
      testErrorMessage: "",
      isTestWaiting: false,
    };
  }

  componentDidMount() { }

  handleCheckServer() {
    alert('Alert from Child. Server check will be done here');
  }

  evaluateFormValid = (prevState) => {
    return ((prevState.hostname.value !== "" && !prevState.hostname.isError) &&
      (prevState.serverIp.value !== "" && !prevState.serverIp.isError));
  };

  handleChange = event => {
    var valResult;
    switch (event.target.id) {
      case 'hostname':
        valResult = PrimaryFormValidator.validateHostname(event.target.value, event.target.labels[0].textContent);
        this.setState({
          ...this.state,
          hostname:
          {
            value: event.target.value,
            isError: valResult.isError,
            errorText: valResult.errorText,
          },
        });
        break;
      case 'serverIp':
        valResult = PrimaryFormValidator.validateIpAddress(event.target.value, event.target.labels[0].textContent);
        this.setState({
          ...this.state,
          serverIp:
          {
            value: event.target.value,
            isError: valResult.isError,
            errorText: valResult.errorText,
          }
        });
        break;
      default:
    }
    this.setState(prevState => ({
      ...prevState,
      isFormValid: this.evaluateFormValid(prevState),
    }));
  }

  render() {
    const { classes } = this.props;

    return (
      <React.Fragment>
        <div className={classes.bigContainer}>
          <Paper className={classes.paper}>
            <div>
              <div>
                <Typography variant="subtitle1" gutterBottom className={classes.subtitle1} color='secondary'>
                  Primary System
              </Typography>
                <Typography variant="body1" gutterBottom>
                  Information related with the primary system.
              </Typography>
              </div>
              <div className={classes.bigContainer}>
                <form className={classes.formArea}>
                  <TextField className={classes.formControl}
                    id="hostname"
                    label="FQDN Hostname *"
                    onChange={this.handleChange}
                    value={this.state.hostname.value}
                    error={this.state.hostname.isError}
                    helperText={this.state.hostname.errorText}
                    variant="outlined" autoComplete="off" />
                  <TextField className={classes.formControl}
                    id="serverIp"
                    label="Server Ip Address *"
                    onChange={this.handleChange}
                    value={this.state.serverIp.value}
                    error={this.state.serverIp.isError}
                    helperText={this.state.serverIp.errorText}
                    variant="outlined" autoComplete="off" />
                </form>
              </div>
            </div>
          </Paper>
        </div>
      </React.Fragment>
    )
  }
}
export default withRouter(withStyles(styles)(PrimaryForm));

(пс: я хотел бычтобы решить эту проблему без другого фреймворка, такого как Redux и т. д., если возможно

1 Ответ

1 голос
/ 01 октября 2019

Пример в Typescript. Идея состоит в том, что родитель передает свой обратный вызов ребенку. Ребенок вызывает обратный вызов родителя, предоставляя свой собственный, например, дочерний обратный вызов в качестве аргумента. Родитель сохраняет то, что он получил (дочерний обратный вызов), в переменной члена класса и вызывает ее позже.

import * as React from 'react'

interface ICallback {
  (num: number): string
}

type ChildProps = {
  parent_callback: (f: ICallback) => void;
}

class Child extends React.Component {
  constructor(props: ChildProps) {
    super(props);
    props.parent_callback(this.childCallback);
  }

  childCallback: ICallback = (num: number) => {
    if (num == 5) return "hello";
    return "bye";
  }

  render() {
    return (
      <>
        <div>Child</div>
      </>
    )
  }
}

class Parent extends React.Component {
  readonly state = { msg: "<not yet set>" };

  letChildRegisterItsCallback = (fun: ICallback) => {
    this.m_ChildCallback = fun;
  }

  callChildCallback() {
    const str = this.m_ChildCallback? this.m_ChildCallback(5) : "<callback not set>";
    console.log("Child callback returned string: " + str);
    return str;
  }

  componentDidMount() {
    this.setState((prevState) => { return {...prevState, msg: this.callChildCallback()} });
  }

  render() {
    return (
      <>
        <Child {...{ parent_callback: this.letChildRegisterItsCallback }} />
        <div>{this.state.msg}</div>
      </>
    )
  }

  m_ChildCallback: ICallback | undefined = undefined;
}

PSТот же код в Javascript. Разница лишь в том, что interface, type, readonly и аннотации типов удалены. Вставка в здесь подтверждает, что это действительный код ES2015 stage-2.

class Child extends React.Component {
  constructor(props) {
    super(props);
    props.parent_callback(this.childCallback);
  }

  childCallback = (num) => {
    if (num == 5) return "hello";
    return "bye";
  }

  render() {
    return (
      <>
        <div>Child</div>
      </>
    )
  }
}

class Parent extends React.Component {
  state = { msg: "<not yet set>" };

  letChildRegisterItsCallback = (fun) => {
    this.m_ChildCallback = fun;
  }

  callChildCallback() {
    const str = this.m_ChildCallback? this.m_ChildCallback(5) : "<callback not set>";
    console.log("Child callback returned string: " + str);
    return str;
  }

  componentDidMount() {
    this.setState((prevState) => { return {...prevState, msg: this.callChildCallback()} });
  }

  render() {
    return (
      <>
        <Child {...{ parent_callback: this.letChildRegisterItsCallback }} />
        <div>{this.state.msg}</div>
      </>
    )
  }

  m_ChildCallback = undefined;
}
...