0

My project takes in a display name that I want to save in a context for use by future components and when posting to the database. So, I have an onChange function that sets the name in the context, but when it does set the name, it gets rid of focus from the input box. This makes it so you can only type in the display name one letter at a time. The state is updating and there is a useEffect that adds it to local storage. I have taken that code out and it doesn't seem to affect whether or not this works.

There is more than one input box, so the auto focus property won't work. I have tried using the .focus() method, but since the Set part of useState doesn't happen right away, that hasn't worked. I tried making it a controlled input by setting the value in the onChange function with no changes to the issue. Other answers to similar questions had other issues in their code that prevented it from working.

Component:

import React, { useContext } from 'react';
import { ParticipantContext } from '../../../contexts/ParticipantContext';

const Component = () => {
  const { participant, SetParticipantName } = useContext(ParticipantContext);

  const DisplayNameChange = (e) => {
    SetParticipantName(e.target.value);
  }

  return (
    <div className='inputBoxParent'>
      <input 
        type="text" 
        placeholder="Display Name" 
        className='inputBox'
        onChange={DisplayNameChange}
        defaultValue={participant.name || ''} />
    </div>
  )
}

export default Component;

Context:

import React, { createContext, useState, useEffect } from 'react';

export const ParticipantContext = createContext();

const ParticipantContextProvider = (props) => {
  const [participant, SetParticipant] = useState(() => {
    return GetLocalData('participant', 
      {
        name: '',
        avatar: {
          name: 'square',
          imgURL: 'square.png'
        }
    });
  });

  const SetParticipantName = (name) => {
    SetParticipant({ ...participant, name });
  }

  useEffect(() => {
    if (participant.name) {
      localStorage.setItem('participant', JSON.stringify(participant))
    }
  }, [participant])

  return ( 
    <ParticipantContext.Provider value={{ participant, SetParticipant, SetParticipantName }}>
      { props.children }
    </ParticipantContext.Provider>
  );
}

export default ParticipantContextProvider;

Parent of Component:

import React from 'react'
import ParticipantContextProvider from './ParticipantContext';
import Component from '../components/Component';

const ParentOfComponent = () => {
  return (
    <ParticipantContextProvider>
      <Component />
    </ParticipantContextProvider>
  );
}

export default ParentOfComponent;

This is my first post, so please let me know if you need additional information about the problem. Thank you in advance for any assistance you can provide.

1 Answers1

0

What is most likely happening here is that the context change is triggering an unmount and remount of your input component.

A few ideas off the top of my head:

  1. Try passing props directly through the context provider:
// this
<ParticipantContext.Provider
  value={{ participant, SetParticipant, SetParticipantName }}
  {...props}
/>

// instead of this
<ParticipantContext.Provider
  value={{ participant, SetParticipant, SetParticipantName }}
>
  { props.children }
</ParticipantContext.Provider>

I'm not sure this will make any difference—I'd have to think about it—but it's possible that the way you have it (with { props.children } as a child of the context provider) is causing unnecessary re-renders.

If that doesn't fix it, I have a few other ideas:

  1. Update context on blur instead of on change. This would avoid the context triggering a unmount/remount issue, but might be problematic if your field gets auto-filled by a user's browser.

  2. Another possibility to consider would be whether you could keep it in component state until unmount, and set context via an effect cleanup:

const [name, setName] = useState('');
useEffect(() => () => SetParticipant({ ...participant, name }), [])
<input value={name} onChange={(e) => setName(e.target.value)} />
  1. You might also consider setting up a hook that reads/writes to storage instead of using context:
const useDisplayName = () => {
  const [participant, setParticipant] = useState(JSON.parse(localStorage.getItem('participant') || {}));
  const updateName = newName => localStorage.setItem('participant', {...participant, name} );
  return [name, updateName];
}

Then your input component (and others) could get and set the name without context:

const [name, setName] = useDisplayName();
<input value={name} onChange={(e) => setName(e.target.value)} />
ray
  • 26,557
  • 5
  • 28
  • 27
  • So the on blur worked for fixing the re-rendering issue, but it caused a different issue with tabbing. Pressing tab just focused and un-focused that input which I feel like is an accessibility problem? Maybe I'm overthinking it. I think there's some kind of issue with mounting and unmounting, so the second one started doing some funky stuff. This component is nested pretty deep and this is my first big react project, so that's probably my bad. The third option is intriguing. Would there be a way to add more methods to the hook or should I do that in a separate utility class? Thanks. – Matt Stockton Sep 23 '20 at 17:49