Search code examples
reactjsreact-reduxstatemapstatetoprops

What state data do I need to pass to mapStateToProps


I am trying to put in a form component in my app using Redux for the first time and I am trying to get my head around what I have to create Reducers / Actions for.

In other components I have my user and messages passed into mapStateToProps and they work correctly. However in this component I am pulling data from my backend for table fields in the componentDidMount method and I am not sure if it is only data that is to be changed that get stored in Redux.

Do I need to create a reducer for the form as well? or does it get posted straight to the backend / node / postgresql. I intend to have a table that updates with all the most recent data so I can see the logic of it being automatically added to retrieved data.

I am pretty new to React / JavaScript so my logic may be a bit off so any advice would be appreciated.

diveLogForm.component.js

export class DiveLogForm extends Component  {

        constructor(props){

            super(props);
            this.handleSubmitDive = this.handleSubmitDive.bind(this);
            this.onChangeDiveType = this.onChangeDiveType.bind(this);
            this.onChangeSchoolName = this.onChangeSchoolName.bind(this);
            this.onChangeCurrent = this.onChangeCurrent.bind(this);
            this.onChangeVisibility = this.onChangeVisibility.bind(this);
            this.onChangeDiveDate = this.onChangeDiveDate.bind(this);
            this.onChangeMaxDepth = this.onChangeMaxDepth.bind(this);
            this.onChangeDiverUserNumber = this.onChangeDiverUserNumber.bind(this);
            this.onChangeVerifiedBySchool = this.onChangeVerifiedBySchool.bind(this);
            this.onChangeDiveNotes = this.onChangeDiveNotes.bind(this);
            this.onChangeDivePoint = this.onChangeDivePoint.bind(this);

            this.state = {
                diveTypeID: "",
                diveSchoolNameID: "",
                diveCurrentID: "",
                diveVisibilityID: "",
                diveDate: "",
                diveMaxDepth: "",
                diverUserNumber: "",
                diveVerifiedBySchool: "",
                diveNotes: "",
                divePoint: "",
                currentList: [],
                regionList: [],
                diveTypeList: [],
                visibilityList: [],
                diveSpotList: [],
                currentUser: [],
                loading: false,
            };
        }

        componentDidMount() {
            pullCurrentFields().then((response) => {
                const { data } = response;
                this.setState({ currentList: data.data });
            });
            pullRegionFields().then((response) => {
                const { data } = response;
                this.setState({ regionList: data.data });
            });
            pullDiveTypeFields().then((response) => {
                const { data } = response;
                this.setState({ diveTypeList: data.data });
            });
            pullVisibilityFields().then((response) => {
                const { data } = response;
                this.setState({ visibilityList: data.data });
            });
            pullDiveSpotFields().then((response) => {
                const { data } = response;
                this.setState({ diveSpotList: data.data });
            });

            //this.props.userDiveLogList();
        }

        onChangeDiveType(e) {
            this.setState({
                diveTypeID: e.target.value,
            });

        }

        onChangeSchoolName(e) {
            this.setState({
                diveSchoolNameID: e.target.value,
            });
        }

        onChangeCurrent(e) {
            this.setState({
                diveCurrentID: e.target.value,
            });
        }

        onChangeVisibility(e){
            this.setState({
                diveVisibilityID: e.target.value,
            });
        }

        onChangeDiveDate(e) {
            this.setState({
                diveDate: e.target.value,
            });
        }

        onChangeMaxDepth(e){
            this.setState({
                diveMaxDepth: e.target.value,
            });
        }

        onChangeDiverUserNumber(e){
            this.setState({
                diverUserNumber: e.target.value,
            });
        }

        onChangeVerifiedBySchool(e){
            this.setState({
                diveVerifiedBySchool: e.target.value,
            });
        }

        onChangeDiveNotes(e) {
            this.setState({
                diveNotes: e.target.value,
            });
        }

        onChangeDivePoint(e){
            this.setState({
                divePoint: e.target.value,
            });
        }

        handleSubmitDive(e) {

            e.preventDefault();

            this.setState({
                loading: true,
            });
            this.form.validateAll();

            //const {dispatch, history} = this.props;

            if (this.checkBtn.context._errors.length === 0) {
                this.props
                    .dispatch(registerUserDive(

                        this.state.diveTypeID,
                        this.state.diveSchoolNameID,
                        this.state.diveCurrentID,
                        this.state.diveVisibilityID,
                        this.state.diveDate,
                        this.state.diveMaxDepth,
                        this.state.diverUserNumber,
                        this.state.diveVerifiedBySchool,
                        this.state.diveNotes,
                        this.state.diveNotes))

                    .then(() => {
                        window.history.push("/divelogtable");
                        window.location.reload();
                    })
                    .catch(() => {
                        this.setState({
                            loading: false
                        });
                    });
            }
        }


    render() {

        const { classes } = this.props;
        const { user: currentUser } = this.props;

        if (this.state.currentList.length > 0) {
            console.log("currentList", this.state.currentList);
        }
        if (this.state.regionList.length > 0) {
            console.log("regionList", this.state.regionList);
        }
        if (this.state.diveTypeList.length > 0) {
            console.log("diveTypeList", this.state.diveTypeList);
        }
        if (this.state.visibilityList.length > 0) {
            console.log("visibilityList", this.state.visibilityList);
        }
        if (this.state.diveSpotList.length > 0) {
            console.log("diveSpotList", this.state.diveSpotList);
        }         

        return (

 ...materialUI form code

function mapStateToProps(state){
    const { user } = state.auth;
    const { regionList } = state.region;
    const { currentList } = state.current;
    const { diveTypeList } = state.diveType;
    const { visibilityList } = state.visibility;
    const { diveSpotList } = state.diveSpot;
    return {
        user,
        regionList,
        currentList,
        diveTypeList,
        visibilityList,
        diveSpotList,
    };
}

export default compose(
    connect(
        mapStateToProps,
    ),
    withStyles(useStyles)
)(DiveLogForm);

As I am primarily concerned with adding my form data to the backend. I have included the diveLog.service.js file etc

export const registerDive = (diveTypeID, diveSchoolNameID, diveCurrentID, diveVisibilityID, diveDate, diveMaxDepth, diveEquipmentWorn, diverUserNumber, diveVerifiedBySchool, diveNotes, divePoint) => {
    return axios.post(API_URL + "registerdive", {
        diveTypeID,
        diveSchoolNameID,
        diveCurrentID,
        diveVisibilityID,
        diveDate,
        diveMaxDepth,
        diveVerifiedBySchool,
        diveNotes,
        divePoint
    });
};

diveLog.action.js

export const registerUserDive = (
                                    diveTypeID,
                                    diveSchoolNameID,
                                    diveCurrentID,
                                    diveVisibilityID,
                                    diveDate,
                                    diveMaxDepth,
                                    diverUserNumber,
                                    diveVerifiedBySchool,
                                    diveNotes,
                                    divePoint) => (dispatch) => {

    return registerDive(

                                    diveTypeID,
                                    diveSchoolNameID,
                                    diveCurrentID,
                                    diveVisibilityID,
                                    diveDate,
                                    diveMaxDepth,
                                    diveVerifiedBySchool,
                                    diveNotes,
                                    divePoint).then(

        (response) => {
            dispatch ({
                type: successful_reg,
            });
            dispatch({
                type: set_message,
                payload: response.data.message,
            });
            return Promise.resolve();
        },
        (error) => {
            const message =
                (error.response &&
                    error.response.data &&
                    error.response.data.message) ||
                error.message ||
                error.toString();
            dispatch({
                type: set_message,
                payload: message,
            });

            return Promise.resolve();
        },
        (error) => {
            (error.response &&
                error.response.data &&
                error.response.data.message) ||
            error.message ||
            error.toString();

            dispatch({
                type: failed_reg,
            });
            return Promise.reject();
        }
    );
};

My diveLog register action is likely to be a good bit off as I didn't understand the reducer concept when coding it.


Solution

  • I didn't understand your question until I started playing with the code but now I understand what you are trying to do. You have five different lists (regionList, currentList, etc.). These are likely used to generate the options for a dropdown menu.

    Right now you are selecting all of the lists from your redux store and providing them as props through mapStateToProps. You are never pushing any changes to the redux store with your lists. You are calling functions in your componentDidMount to fetch list data from your backend and storing that data in this.state. This is a bit of a conflict because now we have data in two places. Do we use the list from this.props, or the one from this.state?

    Ultimately it's up to you what data you want to store where. The advantage of storing data in redux is that it can be used by multiple different components all at once. It also allows you to do each call to the backend only one time, but in order to reap that advantage you need to write the calls with a conditional check such that you only make the call if the data doesn't already exist.

    Do I need to create a reducer for the form as well? or does it get posted straight to the backend / node / postgresql.

    I would recommend keeping the form state in the component itself, as the partially-filled form is only used by this component.

    I intend to have a table that updates with all the most recent data so I can see the logic of it being automatically added to retrieved data.

    I'm not sure what is a parent of what, but if this form is shown on a screen with the table then you might want to move the isLoading state up to the parent and update it via a callback passed to props. That way the table component knows when it is loading a new row. Or maybe you dispatch an action to store the new dive to redux when you hit submit (but I would not store it on every keystroke).

    In this component I am pulling data from my backend for table fields in the componentDidMount method and I am not sure if it is only data that is to be changed that get stored in Redux.

    Universal data is a good candidate for redux. So something like a list of all regions does make sense to store in redux, in my opinion.

    I am trying to get my head around what I have to create Reducers / Actions for.

    When you have five different lists that all act similarly, it's good to define generalized actions and action creators that take the name of the list as a variable. It would be good to have a generalized pullFields function too!

    This is somewhat of a sidenote, but it's recommended that anyone who is just starting out should learn with function components and the hooks useSelector and useDispatch instead of class components and connect. Writing components has gotten a lot easier and some of the stuff that you are doing like this.handleSubmitDive.bind(this) can easily be avoided.

    I made an attempt to clean up the repetitions in your code but I didn't address the redux questions. So here's a suggested setup for handling the data fetching with redux. Some of this is a bit "advanced" but I think you should be able to just copy and paste it.

    Define an async thunk action which fetches list data from your API and stores it in redux, but doesn't do anything if the data was already loaded.

    import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
    
    export const requireFieldData = createAsyncThunk(
      'fields/requireData', // action name
      // action expects to be called with the name of the field
      async (field) => {
        // you need to define a function to fetch the data by field name
        const response = await pullField(field);
        const { data } = response;
        // what we return will be the action payload
        return {
          field,
          items: data.data
        };
      },
      // only fetch when needed: https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution
      {
        condition: (field, {getState}) => {
          const {fields} = getState();
          // check if there is already data by looking at the array length
          if ( fields[field].length > 0 ) {
            // return false to cancel execution
            return false;
          }
        }
      }
    )
    ...
    

    Reducer for fields which stores the data fetched from the API. (using createSlice)

    ...
    
    const fieldsSlice = createSlice({
      name: 'fields',
      initialState: {
        current: [],
        region: [],
        diveType: [],
        visibility: [],
        diveSpot: [],
      },
      reducers: {},
      extraReducers: {
        // picks up the success action from the thunk
        [requireFieldData.fulfilled.type]: (state, action) => {
          // set the property based on the field property in the action
          state[action.payload.field] = action.payload.items
        }
      }
    })
    
    export default fieldsSlice.reducer;
    

    The user reducer needs to be able to add a dive. You probably want to store a lot more info and have a lot more actions here.

    const userSlice = createSlice({
      name: 'user',
      initialState: {
        dives: [],
      },
      reducers: {
        // expects action creator to be called with a dive object
        addDive: (state, action) => {
          // append to the dives array
          state.dives.push(action.payload)
        }
      }
    })
    
    export const { addDive } = userSlice.actions;
    export default userSlice.reducer;
    

    Basic store setup joining fields and user slices

    import { configureStore } from "@reduxjs/toolkit";
    import fieldsReducer from "./fields";
    import userReducer from "./user";
    
    export default configureStore({
      // combine the reducers
      reducer: {
        user: userReducer,
        fields: fieldsReducer,
      }
    });
    

    The component can use useSelector instead of mapStateToProps to access data from redux. We will dispatch the thunk action to make sure that all of the lists are loaded. They will start out as empty arrays but get updated to a new value when the action is completed.

    const DiveLogForm = (props) => {
    
      // select user object from redux
      const user = useSelector(state => state.user);
    
      // get the object with all the fields
      const fields = useSelector(state => state.fields);
    
      // can destructure individual fields
      const { current, region, diveType, visibility, diveSpot } = fields;
    
      // state for the current field value
      const [dive, setDive] = useState({
        typeID: "",
        schoolNameID: "",
        currentID: "",
        visibilityID: "",
        date: "",
        maxDepth: "",
        userNumber: "",
        verifiedBySchool: "",
        notes: "",
        point: "",
      });
    
      // all onChange functions do the exact same thing, so you only need one
      // pass to a component like onChange={handleChange('typeID')}
      const handleChange = (property) => (e) => {
        setDive({
          // override the changed property and keep the rest
          ...dive,
          [property]: e.target.value,
        });
      }
    
      // get access to dispatch
      const dispatch = useDispatch();
    
      // useEffect with an empty dependency array is the same as componentDidMount
      useEffect(() => {
        // dispatch the action to load fields for each field type
        // once loaded, the changes will be reflected in the fields variable from the useSelector
        Object.keys(fields).forEach(name => dispatch(requireFieldData(name)));
      }, []); // <-- empty array
    
      const handleSubmitDive = (e) => {
    
        // do some stuff with the form
    
        // do we need to save this to the backend? or just to redux?
        dispatch(addDive(dive));
      }
    
      return (
        <form />
      )
    }