1

I am trying to change a parent's component state from a child component's state.

This is the parent component:

class App extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      selectedLanguage: 'EU',
      repos: null,
      error: null,
      loggedin: false
    }

    this.updateLanguage = this.updateLanguage.bind(this)
    this.logIn = this.logIn.bind(this)
  }

  componentDidMount () {
    this.updateLanguage(this.state.selectedLanguage)
  }

  updateLanguage (selectedLanguage) {
    this.setState({
      selectedLanguage,
      error: null
    })

    fetchLanguageRepos(selectedLanguage)
      .then(
        (repos) => this.setState({
          repos,
          error: null,
        })
      )
      .catch(() => {
        console.warn('Error fetching repos: ', error)

        this.setState({
          error: 'There was an error fetching the repositories.'
        })
      })
  }

  logIn() {
    this.setState({
      loggedin: true
    })
  }

  render() {
    const { selectedLanguage, repos, error, loggedin } = this.state

    console.log(loggedin)

      return (
        <Router>
          <div className='container'>
            <LanguagesNav
              selected={selectedLanguage}
              onUpdateLanguage={this.updateLanguage}
            />
            <Route
              exact path='/'
              render={(props) => (
                <Login
                  repos={repos}
                  selectedLanguage={selectedLanguage}
                  logIn={this.logIn}
                />
              )}
            />
            <Route
              path='/dashboard'
              render={(props) => (
                <Dashboard
                  repos={repos}
                  selectedLanguage={selectedLanguage}
                />
              )}
            />
            <Route
              path='/profile'
              render={(props) => (
                <Profile
                  repos={repos}
                  selectedLanguage={selectedLanguage}
                />
              )}
            />
            <Route path='/newdashboard'>
                <DrawerPage />
            </Route>
          </div>
        </Router>
      )
  }
}

As I saw on this answer to a similar question, I am passing a function setting the state from parent to child, and then, I call the function via props from the child component:

function LoginForm ({ repos, selected }) {
  const languages = ['EU', 'ES', 'EN']

  var language = {}
  switch (selected) {
    case "EU":
      selected = "EU";
      language  = repos[0].terms;
      break;
    case "ES":
      selected = "ES";
      language = repos[1].terms;
      break;
    case "EN":
      selected = "EN";
      language = repos[2].terms;
      break;
  }

  return (
    <ThemeProvider theme={theme}>
      <Grid container component="main" sx={{ height: '100vh' }}>
        <CssBaseline />
        <Grid
          item
          xs={false}
          sm={4}
          md={7}
          sx={{
            backgroundImage: 'url(https://loginsso.ehu.es/login/images/forest.jpg)',
            backgroundRepeat: 'no-repeat',
            backgroundColor: (t) =>
              t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900],
            backgroundSize: 'cover',
            backgroundPosition: 'center',
          }}
        />
        <Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
          <Box
            sx={{
              my: 8,
              mx: 4,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
            }}
          >
            <img
              src="https://loginsso.ehu.es/login/images/logo_UPV_peq.png"
            />
            <br/>
            <Box component="form" noValidate sx={{ mt: 1 }}>
              <TextField
                margin="normal"
                required
                fullWidth
                id="email"
                label={language.username}
                name="email"
                autoComplete="email"
                autoFocus
              />
              <TextField
                margin="normal"
                required
                fullWidth
                name="password"
                label={language.password}
                type="password"
                id="password"
                autoComplete="current-password"
              />
              <FormControlLabel
                control={<Checkbox value="remember" color="primary" />}
                label={language.remember}
              />
              <Link
                to={{
                  pathname: '/dashboard',
                  search: `?lang=${selected}`
                }}
              >
                <Button
                  type="submit"
                  fullWidth
                  variant="contained"
                  sx={{ mt: 3, mb: 2 }}
                  // onClick={() => {
                  //   alert('clicked');
                  // }}
                >
                  {language.login}
                </Button>
              </Link>
              <Grid container>
                <Grid item xs>
                  <Link
                    to={{
                      pathname: '/newdashboard',
                      search: `?lang=${selected}`
                    }} variant="body2">
                    {language.forgot}
                  </Link>
                </Grid>
                <Grid item>
                </Grid>
              </Grid>
            </Box>
          </Box>
        </Grid>
      </Grid>
    </ThemeProvider>
  )
}

LoginForm.propTypes = {
  repos: PropTypes.array.isRequired
}

export default class Login extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      selectedLanguage: 'EU',
      repos: null,
      error: null,
    }

    this.updateLanguage = this.updateLanguage.bind(this)
  }

  componentDidMount () {
    this.updateLanguage(this.state.selectedLanguage)
  }

  updateLanguage (selectedLanguage) {
    this.setState({
      selectedLanguage,
      error: null
    })

    fetchLanguageRepos(selectedLanguage)
      .then(
        (repos) => this.setState({
          repos,
          error: null,
        })
      )
      .catch(() => {
        console.warn('Error fetching repos: ', error)

        this.setState({
          error: 'There was an error fetching the repositories.'
        })
      })

  }

  render() {
    const { selectedLanguage, repos, error } = this.props

    return (
      <React.Fragment>

        {this.props.logIn} //calling the parent's function

        {error && <p>{error}</p>}

        {repos && <LoginForm repos={repos} selected={selectedLanguage}/>}

      </React.Fragment>
    )
  }
}

However, the state won't change.

Julen Clarke
  • 77
  • 10
  • 2
    If the child component you're talking about is `Login`, check your syntax. You're passing props like this: ``. I guess it should be: `logIn={this.logIn}` That's how the method is called in `App` and that's the method you're trying to call in `Login` – lbsn Sep 23 '21 at 10:53
  • 1
    @Julen I advise you to rewrite your code using functional components and hooks, this is the new standard on React. – MB_ Sep 23 '21 at 11:01
  • 1
    `{this.props.logIn} //calling the parent's function` - this isn't calling `logIn`, it would need to have `{this.props.logIn()}`, but I highly doubt you actually want to call this function in your render() method – Nick Parsons Sep 23 '21 at 11:04
  • @NickParsons I tried as you suggested, and even though it does change the state, I got an uncaught TypeError. Where would you call it from? – Julen Clarke Sep 23 '21 at 11:22
  • @MB_ Thanks for the advice, that might be a good solution actually, I will have to learn Hooks though. – Julen Clarke Sep 23 '21 at 11:23
  • @lbsn corrected. – Julen Clarke Sep 23 '21 at 11:24
  • The `componentDidMount` in your `App` component seems redundant. It doesnt' really do anything as the `selectedLanguage` is already initialized as 'EU'. – Nice Books Sep 24 '21 at 10:12

1 Answers1

1

Okay, I found a solution to my question. I tried this on the child component:

function LoginForm ({ repos, selected, onLogIn }) {
  const languages = ['EU', 'ES', 'EN']

  var language = {}
  switch (selected) {
    case "EU":
      selected = "EU";
      language  = repos[0].terms;
      break;
    case "ES":
      selected = "ES";
      language = repos[1].terms;
      break;
    case "EN":
      selected = "EN";
      language = repos[2].terms;
      break;
  }

  return (
    <ThemeProvider theme={theme}>
      <Grid container component="main" sx={{ height: '100vh' }}>
        <CssBaseline />
        <Grid
          item
          xs={false}
          sm={4}
          md={7}
          sx={{
            backgroundImage: 'url(https://loginsso.ehu.es/login/images/forest.jpg)',
            backgroundRepeat: 'no-repeat',
            backgroundColor: (t) =>
              t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900],
            backgroundSize: 'cover',
            backgroundPosition: 'center',
          }}
        />
        <Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
          <Box
            sx={{
              my: 8,
              mx: 4,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
            }}
          >
            <img
              src="https://loginsso.ehu.es/login/images/logo_UPV_peq.png"
            />
            <br/>
            <Box component="form" noValidate sx={{ mt: 1 }}>
              <TextField
                margin="normal"
                required
                fullWidth
                id="email"
                label={language.username}
                name="email"
                autoComplete="email"
                autoFocus
              />
              <TextField
                margin="normal"
                required
                fullWidth
                name="password"
                label={language.password}
                type="password"
                id="password"
                autoComplete="current-password"
              />
              <FormControlLabel
                control={<Checkbox value="remember" color="primary" />}
                label={language.remember}
              />
              <Link
                to={{
                  pathname: '/dashboard',
                  search: `?lang=${selected}`
                }}
              >
                <Button
                  type="submit"
                  fullWidth
                  variant="contained"
                  sx={{ mt: 3, mb: 2 }}
                  // I call the child component's method which at the same time calls the parent's component's method.
                  onClick={() => {onLogIn()}}
                >
                  {language.login}
                </Button>
              </Link>
              <Grid container>
                <Grid item xs>
                  <Link
                    to={{
                      pathname: '/newdashboard',
                      search: `?lang=${selected}`
                    }} variant="body2">
                    {language.forgot}
                  </Link>
                </Grid>
                <Grid item>
                </Grid>
              </Grid>
            </Box>
          </Box>
        </Grid>
      </Grid>
    </ThemeProvider>
  )
}

LoginForm.propTypes = {
  repos: PropTypes.array.isRequired
}

export default class Login extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      selectedLanguage: 'EU',
      repos: null,
      error: null
    }

    this.updateLanguage = this.updateLanguage.bind(this)
    this.logIn = this.logIn.bind(this)
  }

  componentDidMount () {
    this.updateLanguage(this.state.selectedLanguage)
  }

  updateLanguage (selectedLanguage) {
    this.setState({
      selectedLanguage,
      error: null
    })

    fetchLanguageRepos(selectedLanguage)
      .then(
        (repos) => this.setState({
          repos,
          error: null,
        })
      )
      .catch(() => {
        console.warn('Error fetching repos: ', error)

        this.setState({
          error: 'There was an error fetching the repositories.'
        })
      })

  }

  // Created this function that calls the parent's logIn function.
  logIn() {
    this.props.logIn();
  }

  render() {
    const { selectedLanguage, repos, error, logIn } = this.props

    return (
      <React.Fragment>

        {error && <p>{error}</p>}

        {/*I passed child component's method to the function component with the onLogIn attribute.*/}
        {repos && <LoginForm repos={repos} selected={selectedLanguage} onLogIn={this.logIn}/>}

      </React.Fragment>
    )
  }
}

Basically, I created a method on the child component that calls the method of the parent component.

I am not sure if I am using the right words to describe the solution, but I have used this solution in other parts of the project and it always works.

Feel free to comment with suggestions to explain my solution more correctly! :)

Julen Clarke
  • 77
  • 10
  • 1
    What you have done is the precise way to update parent state from child in React. Pass a function (which does the setState) as a prop to the child, -> child calls this prop function -> the parent state is updated. https://dev.to/vadims4/passing-down-functions-in-react-4618 – Nice Books Sep 24 '21 at 10:01