Search code examples
reactjsreduxrxjses6-promiseformik

ReactJS | Component to receive props either as constant or server values with Promises


I have made a small reusable component from using Formik. It is pretty basic. The library's example would make a good starting point, so I am sharing it below as well as the URL:

Code Sandbox for Formik Example

import React from "react";
import { render } from "react-dom";
import { Formik, Field } from "formik";

function Checkbox(props) {
  return (
    <Field name={props.name}>
      {({ field, form }) => (
        <label>
          <input
            type="checkbox"
            {...props}
            checked={field.value.includes(props.value)}
            onChange={() => {
              if (field.value.includes(props.value)) {
                const nextValue = field.value.filter(
                  value => value !== props.value
                );
                form.setFieldValue(props.name, nextValue);
              } else {
                const nextValue = field.value.concat(props.value);
                form.setFieldValue(props.name, nextValue);
              }
            }}
          />
          {props.value}
        </label>
      )}
    </Field>
  );
}

function App() {
  return (
    <Formik
      initialValues={{
        roles: ["admin"]
      }}
      onSubmit={values => alert(JSON.stringify(values, null, 2))}
    >
      {formik => (
        <div>
          <div>
            <Checkbox name="roles" value="admin" />
            <Checkbox name="roles" value="customer" />
          </div>
          <button onClick={formik.submitForm}>submit</button>
          <pre>{JSON.stringify(formik.values, null, 2)}</pre>
        </div>
      )}
    </Formik>
  );
}

render(<App />, document.getElementById("root"));

I am passing instead of hardcoded values a prop of Checkbox, which has the the of Strings (Using PropTypes). This placeholder props is then populating by values that I pass it depending on the usage from a Constants.js file like so.

const NameForCheckbox = {
  Name1: 'Value1',
  Name2: 'Value2',
  Name3: 'Value3',
};

export default NameForCheckbox; // Tha is working just fine.

The Problem: We might in the future get this info from the Server Payload. This will render my approach obsolete, so in order to future proof it, I want to make it render either my data if no server data exist, or bypass them if there are server values.

Now, I was reading up on how to do it and I found Promises able to solve my problem. I tried various things that I won't list here, as I found out by other answers in Stack Overflow not to be correct. I would appreciate if you could help me out here.

MY SETUP: Until now, I did this, which could be totally not usable, but I will list it.

I create 3 Actions, using redux(GetDataAttempt, GetDataSucces, GetDatFailure). Which in return make a GET request to that specific endpoint. That will, in turn, use some RxJS functions like so

import { of } from 'rxjs';
import { switchMap, map, catchError } from 'rxjs/operators';

import { observableFromHttpPromise, combineAndIsolateEpics } from 'utilities/epicsUtil';
import { Type, Actions } from '../actions';

const getDataEpic = (action$, store, deps) =>
  action$.ofType(Type.GET_DATA_ATTEMPT).pipe(
    switchMap(() =>
      observableFromHttpPromise(deps.getData()).pipe(
        map(result => Actions.getDataSuccess(result && result.data)),
        catchError(error => of(Actions.getDataFailure(error && error.message)))
      )
    )
  );

const getAllEpics = combineAndIsolateEpics(getDataEpic);

export default getAllEpics;

Finally the reducers will either send the payload to the component, which I read with MapStateToProps or an empty string.

Now to the tricky part and the reason I asked. How to make this conditional logic apply to the components using Promises. Or anything else.


Solution

  • I'm not sure if I understand how promises change much here.

    If I'm understanding you correctly, you want to display static content in NameForCheckbox unless some server information exists. Obviously fetching this information is asynchronous.

    The simplest possible implementation would be to dispatch your API request (probably via an action) when the component mounts. In the meantime, you can display the values in NameForCheckbox using some simple conditional logic to display either the API data if it exists, or the checkbox otherwise.

    import NameForCheckbox from './NameForCheckbox';
    
    class Checkboxes extends React.Component {
    
      componentDidMount = () => {
        // dispatch async action here that will eventually populate the
        // apiData prop below
      }
    
      renderCheckboxes = data => {
        // insert rendering logic here
      }
    
      render() {
        const { apiData } = this.props;
        const data = someData || NameForCheckbox;
    
        return this.renderCheckboxes(data);
      }
    }
    
    const mapStateToProps = state => ({
      apiData: state.reducer.data || null;
    })
    

    Notice how data falls through to the default data when apiData is falsy (which both null or '', like you suggested, are), but when the data from the API exists in state, it will render that instead.

    It's also pretty common to set a loading animation while you wait for the data to return, so you can also start with an initial state of loading = true, then set loading = false when the API action successfully completes.

    // In reducer
    const initialState = {
      loading: true,
      apiData: null
    }
    
    // In component
    render() {
      const { apiData, loading } = this.props;
    
      if (loading) return <Loading />;
    
      return this.renderCheckboxes(apiData);
    }