Search code examples
reactjsreact-bootstrapformik

Despite using useLayoutEffect, it could still see flickering in the component


I'm using react-bootstrap and formik.

The below UpdateWorkloadForm component is rendered in a Modal's body.

import React, { useImperativeHandle, useLayoutEffect, useReducer, forwardRef } from 'react';
import PropTypes from 'prop-types';
import { Form, Row, Col } from "react-bootstrap";

import "react-datetime/css/react-datetime.css";

import { useFormik} from 'formik';
import * as yup from "yup";
import Datetime from 'react-datetime';
import moment from 'moment';
import DivSpacing from '../common/divSpacing';
import { reducer } from '../common/reducer'
import { TASKSTATUSOPTIONS } from '../data/constants';

// Initial states of UpdateWorkloadForm
const initialState = {
    workflowTasks: null,
};

const UpdateWorkloadForm = forwardRef((props, ref) => {
    // State Handling
    const [state, dispatch] = useReducer(reducer, initialState);

    /***
     * Rest of the code
     */
    
    // "workflowTask" & "workflowTaskStatus" option selection
    const handleWorkflowTasks = (taskList) => {
        dispatch({ type: "workflowTasks", value: taskList});
        if(props.workloadDetails.task_progression.task_id){
            const statusId = (TASKSTATUSOPTIONS.find(v => { return v.label == props.workloadDetails.task_progression.task_status; })).id;
            formik.setFieldValue("workflowTask", props.workloadDetails.task_progression.task_id);
            formik.setFieldValue("workflowTaskStatus", statusId);
        }else{
            formik.setFieldValue("workflowTask", taskList[0].task_id);
            formik.setFieldValue("workflowTaskStatus", TASKSTATUSOPTIONS[0].id);
        }
    }

    // Fetch the tasks of `workflowDescription` from DB
    const fetchWorkflowTasks = async () => {
        return new Promise(function(resolve, reject){
            props.api.post({
                url: '/fetchWorkflowTasks',
                body: {
                    workflowDescription: props.workloadDetails.Description,
                }
            },  (res) => {
                if(res.tasks){
                    handleWorkflowTasks(res.tasks);
                    resolve();
                }
            }, (err, resp) => {
                reject("Oops! Couldn't fetch tasks from DB.");
            });
        });
    }

    useLayoutEffect(() => {
        if(Object.keys(props.workloadDetails).length !== 0){
            fetchWorkflowTasks();
        }
    },[]);

    return (
        <>
            <Form noValidate onSubmit={formik.handleSubmit}>
                <Row>
                    <Col>
                        <Col>
                            <Form.Group controlId="workflowTask">
                                <Form.Label>Task</Form.Label>
                                <Form.Control
                                    as="select"  
                                    name="workflowTask"
                                    onChange={formik.handleChange}
                                    value={formik.values.workflowTask}
                                    isInvalid={formik.touched.workflowTask && formik.errors.workflowTask}
                                >
                                {   state.workflowTasks &&
                                    state.workflowTasks.map((item, index) => (
                                        <option key={item.task_id} value={item.task_id}>
                                            {item.task_name}
                                        </option>
                                    ))
                                }
                                </Form.Control>
                                <Form.Control.Feedback type="invalid">
                                    {formik.errors.workflowStartingPoint}
                                </Form.Control.Feedback>
                            </Form.Group>
                        </Col>
                    </Col>
                </Row>
                <Row>
                    <Col>
                        <Col>
                            <Form.Group controlId="workflowTaskStatus">
                                <Form.Label>Task Status</Form.Label>
                                <Form.Control
                                    as="select"  
                                    name="workflowTaskStatus"
                                    onChange={formik.handleChange}
                                    value={formik.values.workflowTaskStatus}
                                >
                                {   TASKSTATUSOPTIONS.map((item, index) => (
                                        <option key={item.id} value={item.id}>
                                            {item.label}
                                        </option>
                                    ))
                                }
                                </Form.Control>
                            </Form.Group>
                        </Col>
                    </Col>
                </Row>
            </Form>
            
        </>
    );
})

UpdateWorkloadForm.propTypes = {
    api: PropTypes.object.isRequired,
    workloadDetails: PropTypes.object.isRequired,
};

export default UpdateWorkloadForm;

My scenario is: When the modal opens, I make an API call to update the select fields in the form.

Since this is DOM mutation, I used useLayoutEffect. But still I could see the flicker when the component is rendered.

What am I doing wrong?

Thanks


Solution

  • All that useLayoutEffect does is that if you set state while the effect is still running, then the resulting render will be done synchronously. It is not going to wait for asynchronous things like fetching data to complete. The typical use case for layout effects is that you want to render something, then immediately measure the on-screen size of what you rendered, and then render something else so the user doesn't see the dom used for the measurement.

    For fetching data, useLayoutEffect is not going to help. Instead, you need to design your component so that it looks good if the data has not yet been fetched. For example, you could render a loading indicator, or if you just want to render nothing you could return null from the component.

    Assuming your state has a .loading property on it, that might look something like this:

    if (state.loading) {
      return <div>Loading...</div>
      // or, return null
    } else {
      return (
        <>
          <Form noValidate onSubmit={formik.handleSubmit}>
            // ...
          </Form>
        </>
      )
    }