Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Efficiently rendering a large number of Redux Form Fields?

I'm having a table of employees and their monthly working hours day by day. Here we can update all the employees hours values in bulk.

A simple math for the current month: 30 days x 50 employees will result in 1500 mounted Redux Form Fields.

Each time a Field is being mounted, Redux Form will dispatch an action for registering the Field in the Redux store. Therefore 1500 events are dispatched.

Debugging with Chrome->Performance tool, I found out that the whole process from mounting through dispatching to rendering the Fields takes about ~4 seconds:

Performance cost

The above performance score is based on the following simple working example I've created with React, Redux, Redux Form, Reselect:

/* ----------------- SAMPLE DATA GENERATION - START ----------------- */
const generateEmployees = length =>
  Array.from({ length }, (v, k) => ({
    id: k + 1,
    name: `Emp ${k + 1}`,
    hours: [8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8]
  }))

const employees = generateEmployees(50)
/* ----------------- SAMPLE DATA GENERATION - END ------------------- */

/* --------------------- INITIALIZATION - START --------------------- */
const { reduxForm, Field, reducer: formReducer } = ReduxForm
const { createStore, combineReducers, applyMiddleware } = Redux
const { Provider, connect } = ReactRedux
const { createSelector } = Reselect

// Creating the Reducers
const employeesReducer = (state = employees) => state
const reducers = {
  form: formReducer,
  employees: employeesReducer
}

// Custom logger.
// The idea here is log all the dispatched action,
// in order to illustrate the problem with many registered fields better.
const logger = ({ getState }) => {
  return next => action => {
    console.log("Action: ", action)
    return next(action)
  }
}

const reducer = combineReducers(reducers)
const store = createStore(
  reducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
  applyMiddleware(logger)
)
/* --------------------- INITIALIZATION - END ----------------------- */

const renderEmployees = employees =>
  employees.map(employee => {
    return (
      <tr key={employee.id}>
        <td>{employee.id}</td>
        <td>{employee.name}</td>
        {employee.hours.map((hour, day) => (
          <td key={day}>
            <Field component="input" name={`${employee.id}_${day}`} />
          </td>
        ))}
      </tr>
    )
  })

const FormComponent = ({ employees, handleSubmit }) => {
  return (
    <form onSubmit={handleSubmit}>
      <h2>Employees working hours for November (11.2018)</h2>
      <p>
        <button type="submit">Update all</button>
      </p>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>Name</th>
            {Array.from({ length: 30 }, (v, k) => (
              <th key={k + 1}>{`${k + 1}.11`}</th>
            ))}
          </tr>
        </thead>
        {renderEmployees(employees)}
      </table>
    </form>
  )
}

const Form = reduxForm({
  form: "workingHours",
  onSubmit: submittedValues => {
    console.log({ submittedValues })
  }
})(FormComponent)

const getInitialValues = createSelector(
  state => state.employees,
  users =>
    users.reduce((accumulator, employee) => {
      employee.hours.forEach(
        (hour, day) => (accumulator[`${employee.id}_${day}`] = hour)
      )

      return accumulator
    }, {})
)

const mapStateToProps = state => ({
  employees: state.employees,
  initialValues: getInitialValues(state)
})

const App = connect(mapStateToProps)(Form)

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
)
table {
  border-collapse: collapse;
  text-align: center;
}

table, th, td {
  border: 1px solid black;
}

th {
  height: 50px;
  padding: 0 15px;
}

input {
  width: 20px;
  text-align: center;
}
<script src="https://unpkg.com/[email protected]/dist/react.js"></script>
<script src="https://unpkg.com/[email protected]/dist/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.4/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-form/6.7.0/redux-form.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/3.0.1/reselect.js"></script>

<div id="root">
    <!-- This element's contents will be replaced with your component. -->
</div>

So am I doing something inefficient and wrong with Redux Form or rendering a large number of Fields at once is bottleneck and I should take a different approach (pagination, lazy loading, etc.)?

like image 452
Jordan Enev Avatar asked Oct 16 '22 11:10

Jordan Enev


2 Answers

Existing open-source form libraries seem to not work well when getting into this kind of complexity. We implemented a number of optimisations by building our own form library, but it's possible that one or more of these ideas you may be able to use with redux form.

We mount a multitude of form & other redux attached components (in the order of thousands), which all do validation when they get mounted. Our architecture required all components to be mounted at once. Each component may need to update the state multiple times when they get mounted. This is not quick, but these are the optimisations we used to make this seem quick for 1000+ controls:

  • Each component mounts in a way which checks if it really needs to dispatch something right now, or if it can wait until later. If the value in the state is wrong, or the current value is invalid - it will still cause a dispatch, otherwise it will defer other updates for later (like when the user focuses it).

  • Implemented a batch reducer which can process multiple actions in a single dispatch. This means if a component does need to perform several actions on mount, at most it will only send 1 dispatch message instead of one dispatch per action.

  • Debouncing react re-renders using a redux batch middleware. This means that react will be triggered to render less often when a big redux update is happening. We trigger listeners on the leading and the falling edge of redux updates, which means that react will update on the first dispatch, and every 500-1000ms after that until there are no more updates happening. This improved performance for us a lot, but depending on how your controls work, this can also make it look a bit laggy (see next item for solution)

  • Local control state. Each form control has a local state and responds to user interaction immediately, and will lazily update the redux state when the user tabs out of the control. This makes things seem fast and results in less redux updates. (redux updates are expensive!!)

I'm not sure any of this will help you, and I'm not even sure that the above was smart or recommended - but it worked really well for us and I hope it can give you some ideas.

Also, if you can get away with mounting less components (pagination etc), you definitely should do that instead. We were limited in our choices by some early architecture choices and were forced to press on and this is what we came up with.

like image 171
caesay Avatar answered Oct 27 '22 10:10

caesay


Did you try https://www.npmjs.com/package/react-virtualized I used it in a project where I was capturing dozen of events. The list is growing and growing and this component helped me render all of them. I'm not sure how Redux Form works but if it is based on mounting then I guess this is a good option.

like image 36
Krasimir Avatar answered Oct 27 '22 09:10

Krasimir