2

An Example I have linked below, that shows the problem I have.

My Problem

I have these two functions

const updatedDoc = checkForHeadings(stoneCtx, documentCtx); // returns object
documentCtx.setUserDocument(updatedDoc); // uses object to update state

and

convertUserDocument(stoneCtx, documentCtx.userDocument);
// uses State for further usage

The Problem I have is, that convertUserDocument runs with an empty state and throws an error and then runs again with the updated state. Since it already throws an error, I cannot continue to work with it.

I have tried several different approaches.

What I tried

In the beginning my code looked like this

checkForHeadings(stoneCtx, documentCtx);
// updated the state witch each new key:value inside the function
convertUserDocument(stoneCtx, documentCtx.userDocument);
// then this function was run; Error

Then I tried the version I had above, to first put everything into an object and update the state only once.

HavingconvertUserDocument be a callback inside of checkForHeadings, but that ran it that many times a matching key was found.

My current try was to put the both functions in seperate useEffects, one for inital render and one for the next render.

  const isFirstRender = useRef(true);

  let init = 0;
  useEffect(() => {
    init++;
    console.log('Initial Render Number ' + init);
    console.log(documentCtx);
    const updatedDoc = checkForHeadings(stoneCtx.stoneContext, documentCtx);
    documentCtx.setUserDocument(updatedDoc);
    console.log(updatedDoc);
    console.log(documentCtx);

    isFirstRender.current = false; // toggle flag after first render/mounting
    console.log('Initial End Render Number ' + init);
  }, []);

  let update = 0;
  useEffect(() => {
    update++;
    console.log('Update Render Number ' + update);
    if (!isFirstRender.current) {
      console.log('First Render has happened.');
      convertUserDocument(stoneCtx.stoneContext, documentCtx.userDocument);
    }
    console.log('Update End Render Number ' + update);
  }, [documentCtx]);

The interesting part with this was to see the difference between Codesandbox and my local development.
On Codesandbox Intial Render was called twice, but each time the counter didn't go up, it stayed at 1. On the other hand, on my local dev server, Initial Render was called only once.
On both version the second useEffect was called twice, but here also the counter didn't go up to 2, and stayed at 1. Codesandbox:
Codesandbox
Local Dev Server:
Locel Dev Server

Short example of that:

let counter = 0;
useEffect(()=> {
counter++;
// this should only run once, but it does twice in the sandbox.
// but the counter is not going up to 2, but stays at 1
},[])

The same happens with the second useEffect, but on the second I get different results, but the counter stays at 1.
I was told this is due to a Stale Cloruse, but doesn't explain why the important bits don't work properly.

I got inspiration from here, to skip the initial render: https://stackoverflow.com/a/61612292/14103981

Code

Here is the Sandbox with the Problem displayed: https://codesandbox.io/s/nameless-wood-34ni5?file=/src/TextEditor.js
I have also create it on Stackblitz: https://react-v6wzqv.stackblitz.io

The error happens in this function:

function orderDocument(structure, doc, ordered) {
  structure.forEach((el) => {
    console.log(el.id);
    console.log(doc);
    // ordered.push(doc[el.id].headingHtml); 
    // if (el.children?.length) {
    //   orderDocument(el.children, doc, ordered);
    // }
  });
  return ordered;
}

The commented out code throws the error. I am console.loggin el.id and doc, and in the console you can see, that doc is empty and thus cannot find doc[el.id].

Someone gave me this simple example to my problem, which sums it up pretty good.

  useEffect(() => {
    documentCtx.setUserDocument('ANYTHING');
    console.log(documentCtx.userDocument);
  });

The Console:

{}
ANYTHING

You can view it here: https://stackblitz.com/edit/react-f1hwky?file=src%2FTextEditor.js


I have come to a solution to my problem.

  const isFirstRender = useRef(true);

  useEffect(() => {
    const updatedDoc = checkForHeadings(stoneCtx.stoneContext, documentCtx);
    documentCtx.setUserDocument(updatedDoc);
  }, []);

  useEffect(() => {
    if (!isFirstRender.current) {
      convertUserDocument(stoneCtx.stoneContext, documentCtx.userDocument);
    } else {
      isFirstRender.current = false;
    }
   
  }, [documentCtx]);

Moving isFirstRender.current = false; to an else statement actually gives me the proper results I want.
Is this the best way of achieving it, or are there better ways?

Mähnenwolf
  • 720
  • 10
  • 30
  • I'm sorry I've only read the first paragraph, but what about changing the first-render-condition to a state-not-initial-condition? – k-wasilewski May 07 '21 at 08:24
  • @k-wasilewski I'm sorry, but what is a `state-not-initial-condition` ? – Mähnenwolf May 07 '21 at 08:29
  • 1
    To check if the state is empty (then do nothing) or if it's updated (then execute this `convertUserDocument`). – k-wasilewski May 07 '21 at 09:00
  • that might work when the object starts out as null, but later when it already has content, it might still encounter errors, because it hasn't been updated. – Mähnenwolf May 07 '21 at 09:03
  • What is the state update you are struggling with with the component render lifecycle? If you just need to not run an effect callback on the initial render then the ref "hack" is the solution. I tend to agree with @k-wasilewski though that starting with different initial state and do a conditional test to run `convertUserDocument`. I think the bigger question/issue may be why you have this such tight coupling between whatever you are updating and the `convertUserDocument` function. Perhaps a more comprehensive component code example would help us understand the issue you trying to solve for. – Drew Reese May 07 '21 at 09:38
  • @DrewReese Is my code example not enough? I am currently also refactoring the code to use a different type of context, with a reducer. The problem I face is that the first function (checkForHeadings) needs to run and be finished before (convertUserDocument) which is dependant on the updated state and should not use the state before that. – Mähnenwolf May 07 '21 at 11:49
  • Sorry, I guess it was only implied, but this seems like an [XY problem](https://en.wikipedia.org/wiki/XY_problem). You have issue X and think solution Y will resolve it, so your question is about an issue with your solution Y instead about the actual issue X you were originally trying to remedy. I was asking to see a bigger code example to see how you are trying to update *some* state, how the state update and additional function call relate, and if there is a more optimal solution. – Drew Reese May 07 '21 at 15:29
  • Have you tried sending `updatedDoc` instead of "waiting" for the state from the *next* render cycle? In other words, in the single `useEffect` pass the same object you are updating state, `convertUserDocument(stoneCtx.stoneContext, updatedDoc);`. – Drew Reese May 07 '21 at 15:33
  • @DrewReese Yes, I have tried that, but that led to the same Error message. So far the solution I have come up with was the only way of achieving my goal. The first render does not have the updated state, so I skipped the first render for `convertUserDocument`. – Mähnenwolf May 10 '21 at 06:59
  • There also isn't more to the code than the part I showed here. The other code I have in my project does not relate to this one. Only `stoneCtx` perhaps, but when the User is on the page, it is just an array and will not be updated on this page. – Mähnenwolf May 10 '21 at 07:02
  • I don't know what error message you refer to. I added `convertUserDocument(stoneCtx, updatedDoc);` to the end of the first effect and commented out the second entirely, and achieve the same document output. See this forked [codesandbox](https://codesandbox.io/s/heuristic-rosalind-gm1bc?file=/src/TextEditor.js). What I'm suggesting should work for your use case since `updatedDoc` is ***exactly*** the same thing you are updating your context `useDocument` state to, but eliminates the need to wait a render cycle for state to update. – Drew Reese May 10 '21 at 16:00

0 Answers0