1

I'm working on a login function in e-commerce React app using Context API. So when user types in his e-mail and password that calls an action in reducer to check that data and set user data as state in Context Provider.

Here's the action (I'm ommitting other cases that work):

const reducer = (state, action) => {
...

switch(action.type) {
case 'USER_LOGIN': 
        let user = userLogin(state.user,action.payload);
        return {
          ...state,
          user: user,
          loggedIn: temp.length> 0 ? true: false
        };

...

Here's the function userLogin() which itself works fine and returns a nice array of user data.

var userLogin = (user, payload) => {

  const url = "/api/users/login.php";
  console.log(payload);
  fetch(url,{
    method: "POST",
    body: JSON.stringify(payload)
  })
      .then(response => response.json())
      .then(
      (result) => {
          user.id = result.id;
          user.name = result.name;
          user.email = result.email;
          user.phone = result.phone;
          user.city = result.city;
          user.street = result.street;
          user.building = result.building;
          user.flat = result.flat;        
      },
      (error) => {
          console.log(error);
      }); 
  return user;
}

But when in result in my <Provider> the value of state of user stays an empty array as initialized.

I think that there's something that has to work with asyncronous type of that fetch(), but couldn't find any reference about that.

UPD: this is my <Login> component

import React, { Component } from 'react'
import {Consumer} from '../../Context'


export default class Login extends Component {
    constructor(props) {
        super(props);
        this.state = {
            email: "",
            password: "",
        };
    }

    userLogin = (dispatch, e) => {
        e.preventDefault();
        dispatch({
            type: 'USER_LOGIN',
            payload: {email: this.state.email,
                      password: this.state.password }
        });
    }

    handleChange = (e) => {
        this.setState({
            [e.target.name] : e.target.value
        })
    }

    render() {
        return (
            <Consumer>
            {
                value=> {
                    const {dispatch} = value;
                    return (
                        <div className="checkout__container">
                        <h5 className="checkout__header">Login</h5>
                        <form>
                            <label className="checkout__label">E-mail:</label><br />
                            <input className="checkout__input" type="email" name="email" onChange={this.handleChange}></input><br /><br />
                            <label className="checkout__label">Password:</label><br />
                            <input className="checkout__input" type="password" name="password" onChange={this.handleChange}></input><br /><br />
                            <button type="button" className="button button-primary" onClick={this.userLogin.bind(this, dispatch)}>Sign In</button>

                        </form> 
                        </div>
                    );
                }
            }


            </Consumer>
        )
    }
}

UPD #2: Here's extract from my Context.js with Provider component

import React, { Component } from 'react'
import axios from 'axios'

const Context = React.createContext();

const reducer = (state, action) => {
...
switch(action.type) {
case 'USER_LOGIN': 
        let user = userLogin(state.user,action.payload);
        return {
          ...state,
          user: user
        };
  }; 
};

var userLogin = (user, payload) => {
    // Do fetch() here and return user array
}
...
class Provider extends Component {
    constructor(props) {
        super(props);
        this.state = {
          user: [],
          loggedIn: false,
          dispatch: action => this.setState( state => reducer(state,action))         
        };
    }
 render() {
    return (
      <Context.Provider value={this.state}>
          {this.props.children}
      </Context.Provider>
    )
  }
}

const Consumer = Context.Consumer;

export {Provider, Consumer};
Sergei Klinov
  • 730
  • 2
  • 12
  • 25

1 Answers1

0

The problem with your code is here

var userLogin = (user, payload) => {
    //...
    fetch(url,{
        method: "POST",
        body: JSON.stringify(payload)
    })
    .then(response => response.json())
    .then(
        (result) => {
            user.id = result.id;
            user.name = result.name;
            // ...     
        },
        (error) => {
            console.log(error);
        }); 
    return user;
}

You call fetch which returns Promise. When Promise will be resolved, callback passed to then will be called. But userLogin will not stop execution until Promise is resolved. So just after fetch called, userLogin returns. But user is not changed at that time. Response not received and Promise is not resolved.

When response comes, user will be changed but return statement has already been executed, so nothing will be returned.

The only way how to correct this is to follow idea of Redux and move async code to action creator. In your case action creator is userLogin from Login component and leave reducer code fully synchronious. You may also move userLogin method out from Login component.

For example

userLogin = (dispatch, e) => {
    e.preventDefault();

    let payload = {email: this.state.email,
                  password: this.state.password };
    const url = "/api/users/login.php";
    console.log(payload);
    fetch(url,{
        method: "POST",
        body: JSON.stringify(payload)
    })
    .then(response => response.json())
    .then(
        (result) => {
            user.id = result.id;
            user.name = result.name;
            // ...
            // And call dispatch from here
            dispatch (type: 'USER_LOGIN',
                user: user // We don't need payload here as we already got user info
            );
        },
        (error) => {
            console.log(error);
        }); 
}

And modify reducer accordinately

const reducer = (state, action) => {
//...
    switch(action.type) {
        case 'USER_LOGIN': 
            return {
                ...state,
                user: [...user, action.user]; // user is array so add new user to existing array
            };
    }; 
}
Fyodor Yemelyanenko
  • 11,264
  • 1
  • 30
  • 38
  • Thanks, now I get user array passed to `reducer`, but `state` is set to an empty array instead of `action.user`. `case 'USER_LOGIN': return { ...state, user: action.user };` I replace `user` with `action.user` because it's the user logged in, there should be only one. – Sergei Klinov Jun 21 '19 at 08:59
  • What are the contents of `user` before dispatch call? – Fyodor Yemelyanenko Jun 21 '19 at 09:39
  • I tried 2 ways: `user: []` and `user:undefined` in `Provider` `state`. The first doesn't change after `dispatch`, and second sets `user` to and empty array – Sergei Klinov Jun 21 '19 at 10:00
  • Much more important what is passed to dispatch. Please `console.log(user)` before calling dispatch – Fyodor Yemelyanenko Jun 21 '19 at 13:15
  • before calling dispatch `user` is an array with correct user data returned from database. The same is when `console.log(action.user)` in the `reducer()` – Sergei Klinov Jun 21 '19 at 14:21