Implement Redux as Vanilla JS in a Single React Component

I’m currently working on a project where I’m patching a few React components piecemeal into an existing ASP.NET build. Because I’m limited in how much I can change in the overall project configuration, and in how many external libraries I can include, I’m trying to avoid asking to get Redux included until I can’t live without it.

However, one of my components has a fairly complicated data structure where only using props and callbacks is leading me down the road to ruin — is there any way I can solve this using Redux principles without including Redux?

The Problem

I’m creating a form builder where users can add questions, and then add options to those questions. A simplified version of my basic data structure looks something like this:

const form = {
name: '',
questions: [
{
title: '',
id: '',
options: [
{id: '', title: ''},
...etc
]
},
...etc
]
}

The form includes an array of questions (each with a unique ID), and each question includes an array of options (each with a unique ID). The functionality required means I need to be able to add/edit/remove questions, and add/edit/remove the options within a question.

A simplified version of my component structure looks like this:

<Form formData={}>
{this.state.questions.map(q => (
<Question>
{q.options.map(o => (
<Option />
)
<Question />
}
</Form>

Redux Principles

If I’m going to use a Redux-flavored solution, I’ll need the following:

  • A reducer which takes a state and an action and returns a new state
  • Actions which describe the change
  • Action Dispatchers which populate the actions and send them through to the reducer

This gets repetitive for handling the add/edit/remove for the options (it’s just an extension of the principles used for handling the questions), so I’m just going to mock out the question functionality in this example.

The Actions

I’ll make a simple object to hold the constants for my action types.

const ACTIONS = {
FORM_EDIT: 'form/edit',
QUESTION_ADD: 'question/add',
QUESTION_EDIT: 'question/edit',
QUESTION_REMOVE: 'question/remove'
}

The Action Dispatchers

I’ll need some functions that take the necessary data and return a Redux action I can run through the reducer.

// the data here would be { name: '' } from an input  
const dispatchEditForm = data => ({
type: ACTIONS.FORM_EDIT,
payload: data
})
// attach a new question to push into the array
const dispatchAddQuestion = question => ({
type: ACTIONS.QUESTION_ADD,
payload: question
})
// get the question by id, and then patch in the data
const dispatchEditQuestion = (id, data) => ({
type: ACTIONS.QUESTION_EDIT,
questionID: id,
payload: data
})
// filter out the question matching the ID
const dispatchRemoveQuestion = id => ({
type: ACTIONS.QUESTION_REMOVE,
id: id
})

The Reducer

I’m going to make the top level component’s React state the state in my reducer. Each time I trigger an action I’ll pass in this.state and I’ll get a new state object back that I’ll use to update the component viathis.setState(state).

Note: I’ve got a later post dealing with improving the logic in the reducer.

const initialState = {
name: '',
questions: []
}
function formReducer(state = initialState, action = {}){
switch(action.type) {
case ACTIONS.FORM_EDIT:
// merge the new data into the object
return { ...state, { data }}
    case ACTIONS.QUESTION_ADD:
// add a new question into the question array
return { ...state, { questions: questionList }
    case ACTIONS.QUESTION_EDIT:
// update the data of the matching question id
return { ...state, { questions: updatedQuestions }
    case ACTIONS.QUESTION_REMOVE:
// filter the matching question out of the list
return { ...state, { questions: prunedQuestions }
    default:
return state
}
}

The Form Component

The form component should support editing an existing form, or creating a new one from scratch, so we’ll handle populating the initial data by allowing formData to be passed in as props.

import React, { Component } from 'react'
export default class FormComponent extends Component {
constructor(props){
super(props)
this.state = FormReducer(this.props.formData)
}
  addQuestion(question){
const state = FormReducer(
this.state,
dispatchAddQuestion(question)
)
this.setState(state)
}
  editForm(data){
const state = FormReducer(this.state, dispatchEditForm(data))
this.setState(state)
}
  editQuestion(id, data){
const state = FormReducer(
this.state,
dispatchEditQuestion(id, data)
)
this.setState(state)
}
  removeQuestion(id){
const state = FormReducer(
this.state,
dispatchRemoveQuestion(id)
)
this.setState(state)
}
  render() {
return (
<div>
<input
onChange={e => this.editForm({name: e.target.value})
value={this.state.name} />
        {this.state.questions.map((q, i) => (
<Question
data={q}
edit={this.editQuestion.bind(this)}
key={i}
remove={this.removeQuestion.bind(this)} />
}
        <button onClick={this.addQuestion.bind(this)>Add</button>
</div>
)
}
}

The Question Component

The question component can be a dumb component, since all the logic is handled by its parent.

import React from 'react'
export const Question = ({data, edit, remove}) => (
<div>
<input
onChange={e => edit(data.id, {title: e.target.value})}
value={data.title} />
<button onClick={() => remove(data.id)}>Remove</button>
</div>
)
Question.propTypes = {
data: React.PropTypes.shape({
id: React.PropTypes.string.isRequired,
title: React.PropTypes.string,
options: React.PropTypes.array
}),
edit: React.PropTypes.func.isRequired,
remove: React.PropTypes.func.isRequired
}

That’s it! Redux-style data management without Redux.