简体   繁体   中英

React useEffect does not work as expected

 useEffect(() => { const method = methodsToRun[0]; let results = []; if (method) { let paramsTypes = method[1].map(param => param[0][2]); let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes); //this is emscripten stuff let params = method[1].map(param => document.getElementById(param[0][0]).value); let result = runAlgo(...params); //this runs the emscripten stuff results.push(...(JSON.parse(result))); setGlobalState('dataOutput', results); } let newMethodsToRun = methodsToRun.slice(1); if (methodsToRun.length>0) { setGlobalState('methodsToRun', newMethodsToRun); } }, [dataOutput, methodsToRun]);

Hello, I am working on a ReactJS app that uses Webassembly with Emscripten. I need to run a series of algorithms from Emscripten in my Js and I need to show the results as they come, not altogether at the end. Something like this:

  • Run first algo
  • Show results on screen form first algo
  • Run second algo AFTER the results from the first algo are rendedred on the screen (so that user can keep checking them)
  • Show results on screen form second algo.....
  • and so on

I already tried a loop that updates a global state on each iteration with timeouts etc, but no luck, so I tried this approach using useEffect, I tried various things but the results keep coming altogether and not one by one. Any clue?

I tried to mock your example HERE :


import React, { useState, useEffect } from 'react';
import './style.css';

export default function App() {

  const [methodsToRun, setMethodsToRun] = useState([method1, method2]);
  const [globalState, setGlobalState] = useState([]);

  useEffect(() => {
    if (methodsToRun.length <= 0) return;
    const method = methodsToRun[0];
    let results = [];
    if (method) {
      let paramsTypes = method[1].map((param) => param[0][2]);
      let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes); //this is emscripten stuff
      let params = method[1].map(
        (param) => document.getElementById(param[0][0]).value
      );
      let result = runAlgo(...params); //this runs the emscripten stuff
      results.push(...JSON.parse(result));
      setGlobalState((global) => [...global, results]);
    }
    setMethodsToRun((methods) => methods.slice(1));
  }, [methodsToRun]);

  console.log('METHODS TO RUN', methodsToRun);
  console.log('GLOBAL RESULT', globalState);

  return (
    <div>
      <div> Global state 1: {globalState[0] && globalState[0][0]}</div>
      <div> Global state 2: {globalState[1] && globalState[1][0]}</div>
      <div id="1">DIV 1 </div>
      <div id="4">DIV 2</div>
      <div id="7">DIV 3</div>
    </div>
  );
}

const method1 = [
  '1param0',
  [
    ['1', '2', '3'],
    ['4', '5', '6'],
    ['7', '8', '9'],
  ],
];
const method2 = [
  '2param0',
  [
    ['11', '223', '3'],
    ['44', '55', '66'],
    ['77', '88', '99'],
  ],
];

//mock wasm
window.wasm = {
  cwrap:
    (...args) =>
    () =>
      JSON.stringify(args),
};

This example assumes your wasm methods are synchronous, your DOM will update and rerender with a new globalState after each method is executed, the cycle is ensured by the fact you are setting a new methods array sliced by one in the state, that will trigger a rerender and a new execution of the useEffect since it has the methodsToRun array in the deps array. Infinite render loop is prevented by the check if (methodsToRun.length <= 0) return; .

The drawback of this approach though is that if your wasms methods are synchronous and heavy, your UI will be frozen until it returns, and that's a terrible user experience for the end user. I'm not sure what kind of task you are trying to perform there, if it can be made async and awaited ( the logic would be the same ) or if you have to move that on a service worker.

So from what you've explained here, I think the issue can be resolved in 2 ways:

1 is with synchronous requests/process.

Assuming you have 2 functions (AlgoX and AlgoY) you want to run one after the other, you can return the expected response. eg

 return AlgoX(); return AlgoY();

The return keyword will block the running process from continuing until you have result.

Another alternative I will suggest will be to use Self-Invoking-Expression:

 useEffect(() => { ( async function AlgoHandler(){ const method = methodsToRun[0]; let results = []; if (method) { let paramsTypes = method[1].map(param => param[0][2]); let runAlgo = await window.wasm.cwrap(method[0], 'string', paramsTypes); //this is emscripten stuff let params = method[1].map(param => document.getElementById(param[0][0]).value); let result = await runAlgo(...params); //this runs the emscripten stuff results.push(...(JSON.parse(result))); setGlobalState('dataOutput', results); } let newMethodsToRun = methodsToRun.slice(1); if (methodsToRun.length>0) { setGlobalState('methodsToRun', newMethodsToRun); } } )(); }, [dataOutput, methodsToRun]);

Notice where I used async and await in the second solution. Let me know if its helpful.

I'm not sure if this answers your question, but I wrote some example code that may help you fulfill your needs. It will need to be tweaked to fit your use case though, as this should be considered pseudo code:

const initialState = {
  count: 0,
  results: [],
};

function reducer(state, action) {
  switch (action.type) {
    case 'addResult':
      return {
        ...state,
        count: state.count + 1,
        results: [...state.results, action.result],
      };
    default:
      return state;
  }
}

function Example({ methodsToRun }) {
  const [{ count, results }, dispatch] = useReducer(reducer, initialState);

  const next = useCallback(async() => {
    if(methodsToRun.length > count) {
      const result = await methodsToRun[count]();
      dispatch({ type: 'addResult', result });
    }
  }, [count, methodsToRun]);

  useEffect(() => {
    next();
  }, [next]);

  return (
    <div>
      <p>Results:</p>
      <ol>
        {results.map(result => (
          <li key={result/* choose a unique key from result here, do not use index */}>{result}</li>
        ))}
      </ol>
    </div>
  );
}

So basically, my idea is to create a callback function, which at first run will be the method at index 0 in your array of methods. When the result arrives, it will update state with the result, as well as update the counter.

The updated count will cause useCallback to change to the next method in the array.

When the callback changes, it will trigger the useEffect, and actually call the function. And so on, until there are no more methods to run.

I have to admit I haven't tried to run this code, as I lack the context. But I think it should work.

At the end of the day, this solution looks like is working for me:

useEffect(() => {
(
  async function AlgoHandler(){
    if (methodsToRun.length <= 0) {
      setGlobalState('loadingResults', false);
      return;
    }
    const method = methodsToRun[0];
    let paramsTypes = method[1].map((param) => param[0][2]);
    let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes); 
    let params = method[1].map(
      (param) => document.getElementById(param[0][0]).value
    );
    let result = await runAlgo(...params);
    setGlobalState('dataOutput', (prev) => [...prev, ...JSON.parse(result)]);
    await sleep(100);
    setGlobalState('methodsToRun', (prev) => prev.filter(m => m != method));
  })();}, [methodsToRun]);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM