React: Maintaining state for collections

Kier Borromeo
3 min readSep 4, 2017

--

If you’re fairly new to building UIs, specifically with React, deciding the data structure for the states of each item in a collection can be quite tricky. But as you progress and learn techniques, especially this one, hopefully this kind of task becomes less terrifying.

Scenario

Here’s an example of the UI we need to build.

States

  • Uploading
  • Progress
  • Failed
  • Complete (default)

Initializing the state

To do the UI above, this data shape is key:

class Uploader extends React.Component {
state = {
uploading: {},
progress: {},
failed: {}
}
}

That’s it.

Whoa, objects?!

Yes, instead of an array. This makes it easier to access state of a particular data in the collection in our render function. Each of these objects contain each of our data’s IDs as key. To elaborate:

state = {
uploading: {
'352': true,
'475': true
},
progress: {
'352': 64,
'475': 15
},
failed: {
'650': true
}
}

During render, we can access them like so:

files.map((file, i) => {
const uploading = this.state.uploading[files.id]
const progress = this.state.progress[files.id]
const failed = this.state.failed[files.id]
// Our JSX...
})

Pretty cool, huh?

Why not update original object?

Alternatively, we can modify the original object like so:

state = {
files: [{
id: 352,
name: 'Screenshot (204).png',
uploading: false,
progress: false,
failed: false

}, {
id: 54,
name: 'Screenshot (12).png'
}]
}

But I don’t think it’s an ideal practice.

As much as we can, I advise to isolate and normalize state because combining it with the original data makes it harder to read the code. Especially if you’re using plain JavaScript without any way to define types — with FlowType or TypeScript.

In addition, doing so also self-documents this functionality.

Further reading

You might be thinking, how come we use an ID even though they haven’t been created in the backend yet? We generate a random id for that particular data until it’s created. Go ahead and check out uuid.

onUpload = (file) => {
const id = uuid();
this.setState({
files: [...this.state.files, {
id,
name: file.name,
// ...
}],
uploading: {
[id]: true
},
progress: {
[id]: 0
}
})
// Run XMLHttpRequest stuff and do a backflip...

Why not isolate the state?

In some cases, it’s better to store state to each collection’s component. To elaborate, let’s say we can delete our files. We’ll probably have this code:

Uploader.js

class Uploader extends React.Component {
state = {
files: []
}
render() {
return (
<div>
{this.state.files.map((file, i) =>
<File file={file}
index={i}
onRemove={this.handleRemove} />
)}
</div>
)
}
handleRemove = (index) => {
// Redacted for brevity...
}
}

File.js

class File extends React.Component {
state = {
error: '',
removing: false
}

render () {
// Redacted for brevity...
}
handleRemove = () => {
this.setState({ removing: true })
ajax().then(res => {
this.props.onRemove(this.props.index)
}, err => {
this.setState({ removing: false, error: err.message })
})
}
}

This works, right? However, this may not be enough for advanced cases when we start needing the state from the top (i.e., Uploader.js).

You may combine both patterns, and get the best of both worlds.

Self-promotion

While you’re at it, check out react-uploadi. It provides you the primitives you need to build a file upload UI.

--

--