React Multi State Submit Button
Tags: react, redux, ux
Published: 2021-10-07 20:33:53

## A multi state submit button that inter actuate according API response. 

When a form with data is sended, we have two alternatives, do in a blindness way or give the user a feedback of what happen. 

We gather information in fields inside a form and of course whe need a submit button.

The benefits of a submit button instead a normal button is that by default their behaviour is tied to ENTER key in mobile or desktop.

In order to give information of the resulting operation a button with five states is implemented: 

 **normal, loading,  warning, error, success**

Every status explain by self.

For dialog an icons will be use MATERIAL-UI library.


![Dialog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uscxlo2r9txvdeb92ctm.png)

![Button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iyjkqu21do7tre79qx8r.png)

![Button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/olu0j96cho3m0xnppq50.png)


## Table of Contents
* [1-Tuning Icons](#icons)
* [2-Multistate Button](#button)
* [3-Dialog with Button](#dialog)
* [4-Flask Dummy route](#flask)



### 1) Tuning Icons <a name="icons"></a>


The MATERIAL-UI library have a large collection of useful icons.

Three icons are needed:

* `CheckCircleIcon` for submit and save confirmation.
* `SyncIcon` for processing.
* `SyncProblemIcon` for error and warning.

In a normal case to use the icons only need to import them :

```javascript
import CheckCircleIcon from '@material-ui/icons/CheckCircle';
import SyncIcon from '@mui/icons-material/Sync';
import SyncProblemIcon from '@mui/icons-material/SyncProblem';
```

But in this case, we need to extract the path to make a fine tunnig, taking from here :

 [material-ui-icons](https://github.com/MODX-Club/material-ui-icons/)

Or from the browser using the right click mouse to inspect the element and then copy the PATH from the SVG.

#### Loading Icon

We will take the path of `SyncIcon` and give a rotation animation :


```javascript
//file: /src/controls/SButtonIcons.jsx
//extract

export function LoadingIcon(props) {
  return (
  <SvgIcon   viewBox="0 0 24 24" style={{ width: 24, height:24  }} > 
   <circle fill="#1976d2" cx="12" cy="12" r="10" />        
        <g transform="translate(2.2 2.2) scale(0.8)"   >
        <path        
         d= 'M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z' 
         fill="#FFFFFF" strokeWidth="0" >
         <animateTransform attributeName="transform" type="rotate" from="360 12 12" to="0 12 12" dur="1.5s" repeatCount="indefinite">
        </animateTransform>
        </path>
        </g>
  
  </SvgIcon>
);
}

```

#### Error-Warning Icon

We will take the path of `SyncProblemIcon` and give a property with the desired fill color. 

```javascript
//file: /src/controls/SButtonIcons.jsx
//extract
export function WarningIcon(props) {
    return (
    <SvgIcon   viewBox="0 0 24 24" style={{ width: 24, height:24  }} > 
     <circle fill={props.color ? props.color :'orange' } cx="12" cy="12" r="10" />        
        <g transform="translate(2.2 2.2) scale(0.8)"   >
        <path    
         d= 'M3 12c0 2.21.91 4.2 2.36 5.64L3 20h6v-6l-2.24 2.24C5.68 15.15 5 13.66 5 12c0-2.61 1.67-4.83 4-5.65V4.26C5.55 5.15 3 8.27 3 12zm8 5h2v-2h-2v2zM21 4h-6v6l2.24-2.24C18.32 8.85 19 10.34 19 12c0 2.61-1.67 4.83-4 5.65v2.09c3.45-.89 6-4.01 6-7.74 0-2.21-.91-4.2-2.36-5.64L21 4zm-10 9h2V7h-2v6z'
         fill="#FFFFFF" strokeWidth="0" >      
        </path>
        </g>
    </SvgIcon>
  );
  }
```

### 2) Multistate Button <a name="button"></a>


The button have a status prop with five possible values:  **normal, loading, warning, error, success**.

To use it, just add: `<SButton status={status} />`, the status is coming from the API response.

Internally the button has a view state, it can be: **idle, wait, timeout**

At the start the view is **idle**. When come a response from the API: **warning, error, success**, the view change to **wait**. The view handle how many seconds the warning, error or success icons are showed.

After 2 second of **wait** view, the button view change to **timeout** restoring the normal button, giving to the user another try. And the fair tale begin again.

```javascript
//file: /src/controls/SButton.jsx

export default class SButton extends React.Component {
    constructor(props) {
      super(props);
      this.state = {view:'idle'};   
    }

  
   //Called immediately after updating occurs. Not called for the initial render.
   componentDidUpdate(prevProps, prevState, snapshot) {

      //console.log("SButton componentDidUpdate, props:",this.props.status);
      
      //Only listen to 'status' change,      
      if (prevProps.status === this.props.status) return; // avoid re call "componentDidUpdate" when view change

      // after firts attemp, change from 'timeout' to 'idle'
      if (this.props.status === 'loading' ) 
      {
        console.log("view: idle");   
        this.setState({view: 'idle'});
      }            

      // ['warning','error','success']
      if (this.props.status === 'warning' ||this.props.status === 'error' || this.props.status === 'success'){          
       
       this.setState({view: 'wait'});// console.log("view: wait");   
       this.timer = setTimeout(()=> {this.setState({view: 'timeout'})}, 2000);
      }
   }

   componentWillUnmount(){
     clearTimeout(this.timer); // console.log("Button componentWillUnmount");     
    }

   render() {      
      
      var icon;

      if (this.state.view==='timeout') 
      { //when timeout, set the normal color to light blue 
        
        icon = <CheckCircleIcon style={{ color: '#1976d2' }}/>
      }
      else //view==[idle or wait], or in first render
      {

      // first render  
      if ( !this.props.status || this.props.status==='normal') {icon = <CheckCircleIcon style={{ color: '#1976d2' }}/> }
      // after submit
      if (this.props.status==='loading' ) {icon = <LoadingIcon/>}
      if (this.props.status==='warning') {icon = <WarningIcon  /> }
      if (this.props.status==='error') {icon = <WarningIcon color={'red' }/> }
      if (this.props.status==='success') {icon = <CheckCircleIcon style={{ color: 'green' }}/> }
      }
       
      // To avoid re-click when status==='loading'
      // type={this.props.status==='normal'?"button":"submit"}

      return (
        <>
        <IconButton {...this.props}  type={this.props.status==='loading'?"button":"submit"}  > 
          {icon}
        </IconButton>       
        </>               
      );
  }
}

```

### 3) Dialog with Submit Button <a name="dialog"></a>

Putting the button at work. For demonstration purposes, the button has been placed in a dialog that exemplifies the creation of a user. 

When the user click the button, an action is dispatched, this submit the data using Axios.The async interaction of the dialogue with the API will be made with Axios and managed by Redux thunk.

The dialog make the API request with Axios, if the API response is 'success' it close by self, if not error advices are showed.


#### handleSubmit

When the user finish to fill fields, click the button and then one action is dispatched.

```javascript
//file: /src/client/ClientCreateDlg.jsx
//extract

  const handleSubmit = (e) => {
    console.log("handleSubmit, data:",data);   
    e.preventDefault();  // prevent a browser reload/refresh
    props.dispatch(actClientCreate(data)); 

  };

```

#### Actions and Reducer


The main action is `actClientsFormApi(data,url)` implementing an async Axios request with Redux Thunk, that will be called when need to create, update or delete a client. In this case just will use `actClientCreate(data)`
Just for demonstration, all three point to the same API route, but in real scenario, every must to have their specific route.
 

```javascript
//file: /src/client/ClientsActions.jsx
//extract

//-----------------------
// Form =>  Create, Update, Delete
//-----------------------
export function actClientCreate(d) {return actClientsFormApi(d,"clientsresponse")};
export function actClientUpdate(d) {return actClientsFormApi(d,"clientsresponse")};
export function actClientDelete(d) {return actClientsFormApi(d,"clientsresponse")};

//------------------------------------------------------
function actClientsFormApi(d,url) { 
   
  return dispatch => {
    dispatch(actClientFormSubmit());// for processing advice msg

    axios.post(process.env.REACT_APP_API_BASE_URL+url,d, {withCredentials: true})
    .then(response => { dispatch(actClientFormResponse(response.data));})
    .catch(error => { dispatch(actClientFormResponse({status:'error',msg:error.message}))})                    
        
  };
}

export const actClientFormInit = () => ({
  type: 'CLIENT_FORM_INIT'  
});
export const actClientFormSubmit = () => ({
  type: 'CLIENT_FORM_SUBMIT'  
});
export const actClientFormResponse = (response) => ({
  type: 'CLIENT_FORM_RESPONSE',
  payload : response
});


```

The reducer is very simple.


```javascript
//file: /src/client/ClientReducer.jsx

const initialState = {
  formStatus : 'normal',
  formMsg: null
};


export default function ClientsReducer(state = initialState,action)
 {
  switch (action.type) {
  
    case 'CLIENT_FORM_INIT':             
        return {
          ...state,
          formStatus: 'normal',
          formMsg: '',          
        };   
    case 'CLIENT_FORM_SUBMIT':   
        return {
          ...state,
          formStatus: 'loading',
          formMsg: '',          
        };   

    case 'CLIENT_FORM_RESPONSE':    
      
      return {
        ...state,
        formStatus: action.payload.status,
        formMsg: action.payload.msg,
        
      };   
  
    default:  
      return state;
  }
}


```
#### The dialog


Working with dialogs, we have an isolated place to do things like create, update or delete items.
We will take the dialog from MATERIAL-UI:

`import Dialog from '@material-ui/core/Dialog';`

As said before, the dialog interactuate with the API dispatching one action.

`props.dispatch(actClientCreate(data)); `

And then, feed the button status prop with the status coming from the API response.

`<SButton status={status} />`

The dialog is connected to redux store, listening the operation status from the API.


```javascript
//file: /src/client/ClientCreateDlg.jsx
//extract
const mapStateToPropsForm = state => ({    
  status:state.clients.formStatus,
  msg:state.clients.formMsg,   
});

export default connect(mapStateToPropsForm)(ClientCreateDlg);

```

Basically the dialog have a form with just three fields, every change over it update the `data` state hook.

```javascript
//file: /src/client/ClientCreateDlg.jsx


function ClientCreateDlg(props){

  const initial = {  name:'',phone:'', mail:'',}; 
  const [data, setData] = useState(initial);  
  
  //Mount - Unmount  
  useEffect(() => {
    props.dispatch(actClientFormInit());  //componentMount    
  
   return () => {
    props.dispatch(actClientFormInit());  //componentWillUnmount      
   };
 }, []);

   //componentDidUpdate  status listener  
   // When success, auto close after some time
  useEffect(() => {
    console.log("status:", props.status);
    var timer;
    if( props.status==='success') 
    {
       timer = setTimeout(() => { props.clbkClose()}, 1000);
    }
    return () => clearTimeout(timer);
     }, [props.status]); 
  
     
  const handleClose = () => {   //console.log("handleClose");     
      props.clbkClose();
  };
  
  const handleChange = (e) => {
    const {name,value} = e.target;
    setData(prevState => ({...prevState,[name]: value}));  
  };

  const handleSubmit = (e) => {
    console.log("handleSubmit:");
    console.log("  data:",data);   
    e.preventDefault();  // prevent a browser reload/refresh
    props.dispatch(actClientCreate(data)); 

  };
      
    const { status, msg } = props; // server API responses   

    var advice = null;         
    if (status === "loading") advice = "Procesing...";    
    if (status === "error") advice =  "Error: " + msg;   
    if (status === "warning") advice =  "Warning: " + msg;      
    if (status === "success") advice = "Data was saved."; 
    
  
    return (
        <Dialog onClose={handleClose}  open={true}>
        <div style={{minWidth:'300px', maxWidth:'400px',minHeight:'200px', padding:"2px",display: "flex" ,flexDirection: "column"}}>
        <DialogTitle ><ViewHeadlineIcon  />Create new Client:</DialogTitle>          
        <form onSubmit={handleSubmit} >
        
        <div style={{minWidth:'50%',boxSizing:'border-box',padding:"2px",display: "flex" ,flexDirection: "column",flexGrow:'1'}}>         
          <TextField name="name"   size="small" placeholder="Name"  onChange={handleChange} />  
          <TextField name="phone"  size="small" placeholder="Phone" onChange={handleChange} />  
          <TextField name="mail"   size="small" placeholder="Mail"  onChange={handleChange} />     
        </div>        

        <div style={{           
           display: "flex" ,                    
           flexDirection: "row",      
           alignItems: "center",
           justifyContent: "space-around" }}> 

        <SButton status={status} />    
        <IconButton  onClick={handleClose}  > <CancelIcon/></IconButton>   
        </div>    

        <Typography  variant='caption' style={{fontWeight:'600',textIndent:'6px'}} noWrap>
          {advice}
        </Typography>                       
       
        </form>
        </div>
        </Dialog>  
    
)

};

const mapStateToPropsForm = state => ({    
  status:state.clients.formStatus,
  msg:state.clients.formMsg,   
});

export default connect(mapStateToPropsForm)(ClientCreateDlg);
```

#### Opening the dialog

The dialog is open from a main control `ClientsMain`. A client add button, trigger the dialog opening. Then use conditional rendering to show or hide it.

```javascript
//file: /src/client/ClientsMain.jsx
class ClientMain extends React.Component {

  constructor(props) {
    super(props);
    this.state = {dlgIsOpen:false}      
  };   
  
  // Handle Dlg Open
  openClientCreateDlg = () =>{
    this.setState({dlgIsOpen:true});
  }

  // Dlg Close Callback
  clbkDlgClose = () => {
    console.log("clbkDlgClose");
    this.setState({dlgIsOpen:false});
  };
   //----------------------------------
 
  render() {   
    //console.log("Client Main render");     
    var renderDlg = null;
    if (this.state.dlgIsOpen){
      renderDlg =  <ClientCreateDlg clbkClose={this.clbkDlgClose} />                                              
    }

    return (    
        <>        
        <IconButton onClick={this.openClientCreateDlg} color="primary"> <AddCircleIcon/>Add new Client</IconButton>           
       {renderDlg}
        </>    
    );
}
}

export default ClientMain;

```


### 4) Flask response dummy route <a name="flask"></a>

In order to simulate the results of the API endpoint, a route with random responses is implemented.

```python

@app.route('/clientsresponse', methods=['POST','GET'])
def clientrandomresponse():
 
  # Real world
  #  json_content = request.get_json(silent=False)
  #  response = doSomething(json_content)
  #  return response

  # Simulation

  responses = [{ 'status': 'success'},
               { 'status': 'error', 'msg': 'Json required'},
               { 'status': 'error', 'msg': 'Missing field '},
               { 'status': 'warning', 'msg': 'Data validation fail'}]

  time.sleep(1) # Just to show Processing Spinner
  return responses[time.localtime().tm_sec%4]  

```

## Conclusion:

Working it that way we achieve a better interaction of the user with the App.
It is important to note that this button can also be applied in inline editing forms.  
      
   Get the full code from  [https://github.com/tomsawyercode/react-multistate-submit-button](https://github.com/tomsawyercode/react-multistate-submit-button)