Hone logo
Hone
Problems

Building a Normalized State Management System in React with TypeScript

Normalized state is a powerful technique for optimizing React applications by reducing redundancy and improving performance. This challenge asks you to implement a simplified normalized state management system, focusing on the core principles of normalization and efficient updates. You'll be building a system that stores data in a flat, relational structure, allowing for faster updates and reduced memory usage.

Problem Description

You are tasked with creating a function normalizeState that takes an initial state object and a mutation function as input. The mutation function will be responsible for updating the state. The normalizeState function should return an object containing:

  1. state: The normalized state object.
  2. update: A function that accepts a mutation object (containing keys and values to update) and returns a new normalized state object. This function should apply the mutation to the normalized state, ensuring that updates are efficient and avoid unnecessary re-renders.

The normalized state should be structured as follows:

  • Entities: A dictionary where keys are unique IDs and values are the corresponding entity objects.
  • Selectors: Dictionaries that map keys to arrays of entity IDs. These selectors define relationships between entities.

The mutation function should be able to update entities and selectors. When an entity is updated, all selectors that reference that entity should also be updated.

Key Requirements:

  • Immutability: The update function must return a new state object. It should not modify the original state.
  • Normalization: The state should be normalized as described above.
  • Efficient Updates: Updates should only affect the parts of the state that are directly impacted by the mutation.
  • TypeScript: The solution must be written in TypeScript with appropriate type definitions.

Expected Behavior:

The update function should accept a mutation object with the following structure:

{
  entities?: { [id: string]: any }; // Updates to entities (id -> new values)
  selectors?: { [selectorName: string]: string[] }; // Updates to selectors (selectorName -> new entity IDs)
}

Examples

Example 1:

// Initial State
const initialState = {
  entities: {
    '1': { name: 'Alice', age: 30 },
    '2': { name: 'Bob', age: 25 },
  },
  selectors: {
    'friends': ['1', '2'],
  },
};

// Mutation: Update Alice's age
const mutation = {
  entities: {
    '1': { ...initialState.entities['1'], age: 31 },
  },
};

const { state, update } = normalizeState(initialState, mutation);

// Expected Output (state.entities['1'].age should be 31)
// state.entities: { '1': { name: 'Alice', age: 31 }, '2': { name: 'Bob', age: 25 } }
// state.selectors: { 'friends': ['1', '2'] }

Example 2:

// Initial State
const initialState = {
  entities: {
    '1': { name: 'Alice' },
    '2': { name: 'Bob' },
  },
  selectors: {
    'friends': ['1', '2'],
  },
};

// Mutation: Add a new selector
const mutation = {
  selectors: {
    'family': ['1'],
  },
};

const { state, update } = normalizeState(initialState, mutation);

// Expected Output (state.selectors.family should contain ['1'])
// state.entities: { '1': { name: 'Alice' }, '2': { name: 'Bob' } }
// state.selectors: { 'friends': ['1', '2'], 'family': ['1'] }

Example 3: (Edge Case - Updating a non-existent entity)

// Initial State
const initialState = {
  entities: {
    '1': { name: 'Alice' },
  },
  selectors: {
    'friends': ['1'],
  },
};

// Mutation: Update a non-existent entity
const mutation = {
  entities: {
    '3': { name: 'Charlie' },
  },
};

const { state, update } = normalizeState(initialState, mutation);

// Expected Output:  The state should remain unchanged, as the entity '3' doesn't exist.
// state.entities: { '1': { name: 'Alice' } }
// state.selectors: { 'friends': ['1'] }

Constraints

  • The initial state must be an object with entities and selectors properties.
  • Entity IDs must be strings.
  • Selector names must be strings.
  • The mutation object must conform to the structure described above.
  • The solution should be reasonably performant for a small to medium-sized state (up to 100 entities and 10 selectors). Optimization for extremely large datasets is not required.

Notes

  • Consider using immutable data structures (e.g., spread operator, Object.assign) to ensure immutability.
  • Think about how to efficiently update selectors when entities are modified.
  • Focus on the core principles of normalization and immutability. Error handling and extensive validation are not required for this challenge.
  • The any type is used for simplicity in the mutation object. In a real-world application, you would want to define more specific types for the entity values.
Loading editor...
typescript