skip to Main Content

I’m simply not understanding why is this happening or even how to explain it, I have this custom hook:

const useFeatures = (existingFeatures: Ft[]) => {
    console.log('fts provided to state:', existingFeatures)
    const [features, setFeatures] = useState(existingFeatures)
    console.log('fts given by state:', features)

 ...

and the output is

 LOG  fts provided to state: [{"done": true, "ftName": "task", "name": "T"}]
 LOG  fts given by state: [{"ftName": "number", "label": "N", "name": "", "value": 22}]

This is happening between renders of different components that uses the useFeatures custom hook and provide different "features" (initial states), its like useState is caching the previous state even if is re initialized.
I also should clarify that I’m developing a React Native App.
I know isn’t much information to post a question, but I’ve been not able to reproduce the bug outside my project.
Just wanna know if someone have experienced something similar.

Ive tried run the second console.log after a timeout or even inside a useEffect hook that takes as parameter [features]
and still got the same log

EDIT

I have some "records" that come from a useQuery realm hook

    const records = useQuery(
        RecordSchema,
        recs => recs.filtered('date == $0', strDateFmt(dayOn)),
        [dayOn],
    ).map(r => ({
        ...r,
        features: r.features.map(f => JSON.parse(f)),
    }))

useFeatures is used by EditableRecord

            {records.map((record, i) => (
                <EditableRecord key={i} existingRecordId={record._id} />
            ))}

EditableRecord.tsx

const existingRecord = useObject<RecordSchema>('Record', existingRecordId)

    const { features, dispatchFeatures } = useFeatures(
        existingRecord?.features.map(f => JSON.parse(f)) || [],
    )

if I update dayOn (that comes from a useState hook) multiple times, the records are rendered with the features of the first record generating this unexpected log

 LOG  fts provided to state: [{"color": "#844", "ftName": "tag", "name": "Tg"}]
 LOG  fts given by state: [{"content": "Ssss", "ftName": "text", "name": "", "prompt": "P"}]
 LOG  fts provided to state: [{"ftName": "number", "label": "N", "name": "", "value": 22}]
 LOG  fts given by state: [{"content": "Ssss", "ftName": "text", "name": "", "prompt": "P"}]
 LOG  fts provided to state: [{"done": true, "ftName": "task", "name": "T"}]
 LOG  fts given by state: [{"content": "Ssss", "ftName": "text", "name": "", "prompt": "P"}]

3

Answers


  1. The default value for the useState hook is only the initial value of state. Once you update state by calling setFeatures it is totally expected that the two console logs will read different values until the hook is un-mounted. (unless of course setFeatures is called again with the initial value).

    Edit: Now that you have added more context, I think setting the key of the EditableRecord to record._id is likely to fix your issue. Keys are how React keeps track of the components that are returned from a map/array. Using the array index instead of a truly unique key can lead to bugs where react doesn’t know a component should be updated.

    <EditableRecord key={record._id} existingRecordId={record._id} />
    
    Login or Signup to reply.
  2. This is why you don’t use a loop index as a key. If all you had to do was pass in a loop index as the key prop, then React could easily do that for you. It wants you to pass a key that means something to your data.

    What’s happening here is that when react renders and sees an element with a key that matches an element at the same spot from the last render, it assumes it’s the exact same component.

    Let’s look at this code.

    const records = useQuery(
      RecordSchema,
      recs => recs.filtered('date == $0', strDateFmt(dayOn)),
      [dayOn],
    ).map(r => ({
      ...r,
      features: r.features.map(f => JSON.parse(f)),
    }))
    
    return <>
      {records.map((record, i) => (
        <EditableRecord key={i} existingRecordId={record._id} />
      ))}
    </>
    

    So say it renders with a single record in records. It will render the equivalent of:

    <EditableRecord key={0} existingRecordId={record._id} />
    

    Now dayOn changes, and records now contains a single totally different record. The key will still be 0, and so that same component will be used with new props. The new record id is passed in as prop, and sent to the initial value of a state that was already setup, and so it is ignored.


    You need to tell React that if the record changes, then it needs to destroy and recreate that component from scratch. And that’s done by using a unique key.

    Try this:

    <EditableRecord key={record._id} existingRecordId={record._id} />
    

    The best practice is to always use a key that uniquely identifies the data that is being rendered by each iteration of the loop. For database records, that’s an id. If it’s a list of names that you know will be unique, then that name could work.

    And if it’s a simple list that renders no internal hooks or state and probably won’t change after its initial render, then maybe you can use the loop index as a key. But this is a last resort, or else bugs like this one make you feel like you are going crazy happen.

    Login or Signup to reply.
  3. Please show where are you calling to useFeatures. If you are using 2 version of the same hook, make sure you are calling the correct function from the parent.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search