2

I am brand new to react, so I apologize if this is a very basic question. I wasn't able to find the answer anywhere.

I have an ExtensionsContext component that stores an array of Extension class objects as the state.

import React, { useContext, useState } from 'react'

const ExtensionsContext = React.createContext()

export function useExtensions() {
    return useContext(ExtensionsContext)
}

export function ExtensionsProvider ({ children }) {
    const [exts, setExtensions] = useState(startingExtensions) // startingExtentions is an array of Extention class objects

    AppExtensions = {
        extensions: exts,
        update: function () { setExtensions(this.extensions) }
    }

    return (
        <ExtensionsContext.Provider value={AppExtensions}>
            {children}
        </ExtensionsContext.Provider>
    )
}
class Extension { 
    constructor (name, index) {
        this.name = name
        this.isOpen = false
        this.id = index
    }
    open () {
        this.isOpen = true
        AppExtensions.update()
    }
    close () {
        this.isOpen = false
        AppExtensions.update()
    }
}

These extensions are being used in a component that lists and groups them between open and closed. Currently, if I call the open method on one of the Extensions, it will change the extension value, but not re-render any components. I currently have it set up so that when the button that is clicked, it toggles a boolean state in the component between true and 1 to force a re-render, but I know I am doing something wrong and that there is a better solution. This also doesn't work if I am using these extensions in multiple places. I imagine the problem has to do with the fact that I am passing in an AppExtensions object and a copy of exts in rather than directly passing exts in. I want to be able to call extension.open() on a click and handle all of the logic inside the ExtensionsProvider.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459

1 Answers1

1

React detect changes by shallow comparing current and previous values. If it's a primitive (a number for example), it compares the actual value. If it's an object or an array, React compares the reference to that object, and not the actual content, even if it changed.

This means that setting the same array of extensions would do nothing, because it's the same array. It also means that changing a single property would do nothing as well. You need to recreate the array, and you need to recreate the object that changed.

In my example below, each Extension object has the toggle method, which returns a new object based on the previous one, with the isOpen property toggled. The update method recreates the extensions array, by mapping the extensions, and toggling one of the objects.

const { useContext, useState, useCallback } = React

const ExtensionsContext = React.createContext()

function useExtensions() {
  return useContext(ExtensionsContext)
}

class Extension {
  constructor (name, index, isOpen = false) {
    this.name = name
    this.isOpen = isOpen
    this.id = index
  }
  toggle() {
    return new Extension(this.name, this.id, !this.isOpen);
  }
}

const createStartingExtensions = () =>
  ['X', 'Y', 'Z'].map((name, i) => new Extension(name, i))

function ExtensionsProvider ({ children }) {
  const [extensions, setExtensions] = useState(createStartingExtensions) // startingExtentions is an array of Extention class objects
  
  const update = useCallback((target) => { 
    setExtensions(extensions => 
      extensions.map(o => 
        o === target ? o.toggle() : o
      ))
  }, []) // recreate the array of extensions
  
  const appExtensions = {
    extensions,
    update
  }

  return (
    <ExtensionsContext.Provider value={appExtensions}>
      {children}
    </ExtensionsContext.Provider>
  )
}

const List = () => {
  const { extensions, update } = useExtensions()

  return (
    <ul>
    {extensions.map(ext => (
      <li key={ext.id} onClick={() => update(ext)}>{ext.name} {ext.isOpen ? 'open' : 'close' }</li>
    ))}
    </ul>
  )
}

const Demo = () => (
  <ExtensionsProvider>
    <List />
  </ExtensionsProvider>
)

ReactDOM.render(
  <Demo />,
  root
)
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

<div id="root"></div>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209