jellyfith
jellyfith

Reputation: 41

How to properly bind nested objects into text-inputs using redux

I have an object of class Entities that adds itself to an entities reducer using an id as its key. In another class Properties, I need to use each of the entity object's properties to create text inputs.

The issue I'm having is that when I update my input textbox, it takes the first key I type and makes that the value, but it rewrites the full value every time so I always end up with a one character value.

I have a class Entities:

import React from 'react';
import { connect } from 'react-redux';
import { updateSelected, addEntity } from '../actions';

class Entity extends React.PureComponent {
  constructor(props) {
    super(props);
    this.identityContainer = {
      id: "abcdef",
      type: "person",
      properties: {
        name: {
          label: "Name", 
          type: "string", 
          value: "", 
          optional: true
        },
        favorite_icecream: {
          label: "Favorite Ice Cream", 
          type: "string", 
          value: "", 
          optional: true
        }
      }
    }
    this.props.addEntity(this.identityContainer);
  }
  handleSelected() {
    this.props.updateSelected(this.identityContainer.id)
  }
  render() {
    return (
        <div onMouseUp={() => this.handleSelected()}></div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    selectedEntity: state.selectedEntity
  }
}
const mapDispatchToProps = () => {
  return {
    updateSelected,
    addEntity
  }
}
export default connect(mapStateToProps, mapDispatchToProps())(Entity);

Now here is my PropertiesWindow class:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { updateProperty } from '../actions';

class PropertiesWindow extends Component {
    get selectedEntity() {
        return this.props.entities[this.props.selectedEntity];
    }
    handleUpdateEntity(id, name, value) {
        this.props.updateProperty(id, name, value);
    }
    createListItems() {
        for (const [name, property] of Object.entries(this.selectedEntity.properties)) {
            return (
                <li key={this.props.selectedEntity + property.name} className="text-input-container">
                    <label htmlFor={this.props.selectedEntity}>{property.label}</label>
                    <input 
                        type="text" 
                        id={this.props.selectedEntity} 
                        name={name}
                        value={property.value}
                        onChange={e => this.handleUpdateEntity(this.selectedEntity.id, name, e.target.value)}
                    />
                </li>
            )
        }
    }
    render() {
        return (
            <ul>
                {this.createListItems()}
            </ul>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        selectedEntity: state.selectedEntity,
        entities: state.entities
    }
}
const mapDispatchToProps = () => {
  return {
    updateProperty
  }
}

export default connect(mapStateToProps, mapDispatchToProps())(PropertiesWindow);

actions:

export const updateSelected = (id) => {
    return {
        type: 'SELECTED_ENTITY_UPDATED',
        payload: id
    }
}
export const addEntity = (entity) => {
    return {
        type: 'ENTITY_ADDED',
        payload: {...entity}
    }
}
export const updateProperty = (id, property, value) => {
    return {
        type: 'PROPERTY_CHANGED',
        payload: {id, property, value}
    }
}

reducers:

import {combineReducers} from 'redux';

export const selectedEntityReducer = (state = null, action) => {
    switch (action.type) {
        case 'SELECTED_ENTITY_UPDATED':
            return action.payload;
        default: 
            return state;
    }
}

export const entitiesReducer = (state = {}, action) => {
    switch (action.type) {
        case 'ENTITY_ADDED':
            state[action.payload.id] = action.payload;
            return state;
        case 'PROPERTY_CHANGED':
            state[action.payload.id].properties[action.payload.property] = action.payload.value;
            return state;
        default: return state;
    }
}

export default combineReducers({
    selectedEntity: selectedEntityReducer,
    entities: entitiesReducer
});

Upon typing into the textbox I log this from action.payload:
updateProperty: Object { id: "abcdef", property: "name", value: "a" }
updateProperty: Object { id: "abcdef", property: "name", value: "s" }
updateProperty: Object { id: "abcdef", property: "name", value: "d" }
updateProperty: Object { id: "abcdef", property: "name", value: "f" }

If someone could help me understand what I am doing wrong I would really appreciate it. I have been trying to solve this issue many different ways, including using an array for properties and an array for entities instead of objects. Nothing I have tried has made any difference.

EDIT:
I have created a codesandbox here: https://codesandbox.io/s/gracious-leakey-vzio6

Upvotes: 1

Views: 191

Answers (2)

jellyfith
jellyfith

Reputation: 41

So after days of reworking this, I have discovered that the issue is properly changing the values of nested objects. In my code, I tried to change the values of the nested properties object by accessing it with computed property names like

state[action.payload.id].properties[action.payload.property] = action.payload.value

However I found that this will not update the state; or if it does, it does so in a way that does not trigger a re-render. In many other questions I have seen that the spread variable must be used to return an entirely new object, which then triggers re-renders. Take this code for example:

case ENTITY_UPDATED:
    const {id, name, value} = action.payload;
    return {
        ...state,
        [id]: {
            ...state[id],
            properties: {
                ...state[id].properties,
                [name]: {
                    ...state[id].properties[name],
                    value: value
                }
            }
        }
    }

This is the way the state needed to be changed for redux to register the changes. I am not 100% sure why this is the case but it is. If anyone has a firm grasp on it please reach out and comment!

Upvotes: 0

Nemanja Lazarevic
Nemanja Lazarevic

Reputation: 1047

Try sending the actual input value every time that it changes, like this:

<input 
  type="text" 
  id={this.props.selectedEntity} 
  name={name}
  value={property.value}
  onChange={e => this.handleUpdateEntity(this.selectedEntity.id, name, e.target.value)}
 />

The line changed is this one:

onChange={e => this.handleUpdateEntity(this.selectedEntity.id, name, e.target.value)

Upvotes: 1

Related Questions