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.
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);
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' });
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
}
}
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/>
</>
);
}
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}
});
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: []
};
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}
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());
}
}
Triggering list reload
//file: ClientsActions.jsx
export const actClientsSetDirty = () => ({
type: 'CLIENTS_SET_DIRTY'
});
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
});
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,
};
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
};
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);
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
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)