DEV Community

Lionel Marco
Lionel Marco

Posted on

React, injecting dialogs with Redux, CRUD dialogs with Axios Flask Api interaction.

React, injecting dialogs with Redux, CRUD dialogs with Axios Flask API interaction.

Whether we like it or not the dialogues are an important part of our application.They allow us to perform simple or complicated actions in a place apart from the main content.

Either to avoid complexity or laziness we always try to use the minimum amount of third-party libraries. In this case we will only use the classic MATERIAL UI who will be in charge of generating the dialogue.
MATERIAL UI providee a dialog whith basic behaviour, like: closing whith scape key or when click outside, it can go fullscreen and also darken the page behind.

We will control the opening and closing of the dialogue with Redux. Also the asynchronic interaction of the dialog with the API will be handled by Redux. The dialog make the API request, get the API response and then if is all is ok it close by self, if not error advices are showed.

Bassically reduce all to a single line of code:

Opening:
this.props.dispatch(showDialog(ClientEditDlg,{id,someCallback}))

Closing:
this.props.dispatch(actCloseDlg());

It is important to note that this methodology can be applied in other types of controls such as: toast, snack bar, banners, or also side columns contents.

Dialog

Api response

Table of Contents

1) Modal Root component

The ModalRoot is an intermediate attendant component, that will render any component and arguments passed in his props. Placed in the main layout, will receive any dialog from any module.

//file: /src/modal/ModalRoot.jsx

const ModalRoot = ({ dlgComponent, dlgProps }) => {
  if (!dlgComponent) {
    return null 
  }  
  const Dlg = dlgComponent;//just for Uppercase component naming convention
  return <Dlg {...dlgProps} />
}

export default connect(state => state.modal)(ModalRoot);

Enter fullscreen mode Exit fullscreen mode

Tied to his own Redux store, so any action distpached, will be listen and then triger a new render.

1.1) ModalRoot, Actions and Reducer.

Only need two action, one to open and one to close:

//file: /src/modal/ModalActions.jsx

export const showDialog = (dlgComponent,dlgProps) => ({type:  'SHOW_DLG',  dlgComponent, dlgProps });
export const closeDialog = () => ({type:  'CLOSE_DLG' });  

Enter fullscreen mode Exit fullscreen mode

The ModalRoot reducer is very simple, just two actions:

//file: /src/modal/ModalReducer.jsx

const initialState = {dlgComponent: null, dlgProps: {}}

export default  function ModalReducer(state = initialState, action) {
    switch (action.type) {
      case 'SHOW_DLG':
        return { dlgComponent: action.dlgComponent,  dlgProps: action.dlgProps}               
      case 'CLOSE_DLG':
        return initialState        
      default:
        return state
    }
  }

Enter fullscreen mode Exit fullscreen mode

2) Main Layout

The ModalRoot component will be placed in the app main layout, common to all modules. In this case only use the module ClientsMain. But keep in mind that here will be placed the navigation bar and all modules, like notes, orders, invoces. The rendering choice of one or other, will be handled by routing or conditional rendering.

//file: /src/App.js

function App() {
  return (
    <>
    <ModalRoot/>
    <ClientsMain/>     
    </>
  );
}     
Enter fullscreen mode Exit fullscreen mode

3) Content Area

For demostration purposes we will work over a client directory with name, phone and mail. Where we can edit and delete every item, also add a new client. "The classic CRUD".

The files of the client module:

ClientsMain.jsx // Listing
ClientCreateDlg.jsx // Create new
ClientEditDlg.jsx // Edit
ClientDeleteDlg.jsx // Delete confirmation

ClientsActions.jsx //Redux Files
ClientsReducer.jsx //Redux Files

3.1) Data fetching

The client list will be retrieve with Axios from a Flask endpoint. When ClientsMain is mounted, trig data fetch from API, dispatching actClientsFetch().

Fetch Client actions:


//file: /src/clients/ClientsActions.jsx

export function actClientsFetch(f) { 
  return dispatch => {
    dispatch(actClientsFetchBegin());  // for loading message or spinner

    axios.post(process.env.REACT_APP_API_BASE_URL+"clientslist",f,{withCredentials: true} )
    .then(response => { dispatch(actClientsFetchSuccess(response.data.items));})
    .catch(error => { dispatch(actClientsFetchError({status:'error',msg:error.message+', ' + (error.response && error.response.data.msg)}))} );    
  };
}

export const actClientsFetchBegin = () => ({
  type: 'CLIENTS_FETCH_BEGIN'
});

export const actClientsFetchSuccess = items => ({
  type: 'CLIENTS_FETCH_SUCCESS',
  payload: { items: items  }
});

export const actClientsFetchError = msg => ({
  type: 'CLIENTS_FETCH_ERROR',
  payload: { msg: msg}
});

Enter fullscreen mode Exit fullscreen mode

Fetch Clients reducer:

Next lines show a code extracted from the reducer.

//file: /src/clients/ClientsReducer.jsx 
// extract :

  case 'CLIENTS_FETCH_BEGIN':  // "loading" show a spinner or Loading msg      
      return {
        ...state,
        status: 'loading'        
      };
    case 'CLIENTS_FETCH_SUCCESS': // All done: set status and load the items from the API
      return {
        ...state,
        status: 'success',        
        items: action.payload.items,
        isDirty : false
      };
    case 'CLIENTS_FETCH_ERROR':  // Something is wrong
      return {
        ...state,
        status: "error",
        msg: action.payload.msg,
        items: []        
      };


Enter fullscreen mode Exit fullscreen mode

Flask dummy route

Just to simulate a server request, a Flask route returning static data is implemented.

@app.route('/clientslist', methods=['POST','GET'])
def clientlist():
   clients= [    {'id':'1','name':'Client 1','mail':' user1@mail.com','phone':'555-555-111'},
                 {'id':'2','name':'Client 2','mail':' user2@mail.com','phone':'555-555-222'},
                 {'id':'3','name':'Client 3','mail':' user3@mail.com','phone':'555-555-333'},
                 {'id':'4','name':'Client 4','mail':' user4@mail.com','phone':'555-555-444'}]

   return {'items':clients}     
Enter fullscreen mode Exit fullscreen mode

3.2) Automatic reloading:

In order to get data consistence the clients Redux store have a isDirty flag, any action over the clients (create,update,delete) will trigger actClientsSetDirty() changing isDirty flag to TRUE and then trigger data reload.

List reload when data is dirty:

//file: /src/clients/ClientsMain.jsx

 componentDidUpdate(prevProps, prevState) {
    if (this.props.isDirty && this.props.status !== 'loading')    {
      this.props.dispatch(actClientsFetch());
    }    
  }

Enter fullscreen mode Exit fullscreen mode

Triggering list reload

//file: ClientsActions.jsx

export const actClientsSetDirty = () => ({
  type: 'CLIENTS_SET_DIRTY'
});

Enter fullscreen mode Exit fullscreen mode

4) Activity Dialog

The activity dialog is the component that would be injected in the modal root, in this case use the material dialog, but can be any thing: banner, toast, etc...

4.1) Activity Dialog, Actions and Reducer.

The activity could be: create, update or delete clients. Every activity have their related action. In this case all point to the same API route, but in real scenario, every must to have their specific route.

//file: /src/clients/ClientsActions.jsx 
// extract :

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));
                        dispatch(actClientsSetDirty()) ;})
    .catch(error => { dispatch(actClientFormResponse({status:'error',msg:error.message+', ' + (error.response && error.response.data.msg)}))
                    })

  };
}

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

Enter fullscreen mode Exit fullscreen mode

Next lines show a code extracted from the reducer, where are three action related to form submiting.

CLIENT_FORM_INIT initialize the formStatus to normal,
CLIENT_FORM_SUBMIT to show processing message,
CLIENT_FORM_RESPONSE is the API response that could be: 'error' or 'success'.

//file: /src/clients/ClientsReducer.jsx 
// extract :

 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,
       };  


Enter fullscreen mode Exit fullscreen mode

4.2) Activity API interaction

The API response is attended by CLIENT_FORM_RESPONSE. A formStatus is implemented to know the request results from the API. Also a formMsg for API error messages.

//file: /src/clients/ClientsReducer.jsx 
// extract :

  case 'CLIENT_FORM_RESPONSE':    

      return {
        ...state,
        formStatus: action.payload.status, //response from API
        formMsg: action.payload.msg        
      };   

Enter fullscreen mode Exit fullscreen mode

We have three activity dialogs:

ClientCreateDlg.jsx // Create new
ClientEditDlg.jsx // Edit
ClientDeleteDlg.jsx // Delete confirmation

The dialog make the API request, if all is ok it close by self, if not error advices are showed.

All have the same internal structure, the important thing to highlight is the formStatus.

When axios resolve the API response, it trigger CLIENT_FORM_RESPONSE. Then the operation result is stored in formStatus that could be: 'error' or 'success'.

For shortness only show 'ClientsCreateDlg'

//file: /src/clients/ClientsCreateDlg.jsx 
// extract :  

function ClientCreateDlg(props){

  const initial = {  name:'',phone:'', mail:'',}; 
  const [state, setState] = useState(initial);  
  const fullScreen = useMediaQuery('(max-width:500px)');// if width<500 go fullscreen

  useEffect(() => { //Mount - Unmount  
         props.dispatch(actClientFormInit());  //componentMount    
         //console.log("component Mount");
        return () => {
         props.dispatch(actClientFormInit());  //componentWillUnmount    
         // console.log("componentWillUnmount");
        };
      }, []);

  //componentDidUpdate  status listener  
  useEffect(() => {
    console.log("status Update", props.status);
    if( props.status==='success') props.dispatch({type: 'CLOSE_DLG' });  //trigger UnMount             
     }, [props.status]); 

  const handleChange = (e) => {
    const {name,value} = e.target;
    setState(prevState => ({...prevState,[name]: value}));  
  };

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

  const handleCancel = () => {    
    props.dispatch({type: 'CLOSE_DLG' });
  } ;  

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

    var advice = null;         
    if (status === "loading") advice = "Procesing...";    
    if (status === "error") advice =  "Error: " + msg;   
    if (status === "success") {  return null; }        

    return (
    <Dialog onClose={handleCancel} fullScreen={fullScreen} open={true}>
        <div style={{minWidth:'300px',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" }}> 

        <IconButton  type="submit"  >   <CheckCircleIcon  color="primary"/> </IconButton>    
        <IconButton onClick={handleCancel}  > <CancelIcon/></IconButton>   
        </div>       
        <Ad l={advice}/>               
        </form>
        </div>
    </Dialog>
);
}

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

export default connect(mapStateToPropsForm)(ClientCreateDlg);


Enter fullscreen mode Exit fullscreen mode

4.3) Flask response dummy route

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


@app.route('/clientsresponse', methods=['POST','GET'])
def clientrandomresponse():

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

  return responses[time.localtime().tm_sec%4]   # only for demostration

Enter fullscreen mode Exit fullscreen mode

Conclusion:

May be look complex to understand, there are two related mechanisms, one in charge of dialog injections, and other related to API interaction.
Usually an app can have many modules: clients, notes, order, and are used one at time, so all can share the same dialog root component.
In this way can open a dialog from whatever place.

Get the full code from https://github.com/tomsawyercode/react-redux-dialogs-crud

Top comments (0)