Search code examples
javascriptreactjsreduxredux-thunkredux-toolkit

Update client state received from api without refreshing in redux


I have two functions, one where I am able to send an order that updates the users balance amongst some other things, and another which retrieves the users balance for the user to see. Before any orders happen I still need to retrieve the balance for the user to see, thus I have broken my getBalance func from MarketLongFunc.

Using redux-toolkit and redux-thunk I have an ordersSlice.js that looks like this:

export const MarketLongFunc = createAsyncThunk(
  "/order/marketlong",
  async (value, thunkAPI) => {
    const token = thunkAPI.getState().auth.user.token;
    const newObj = {
      value: value,
      token: token,
    };

    let url = `http://localhost:3001/api/orders/marketlong`;
    const response = await axios.post(url, newObj);
    //getBalance() 
    return;
  }
);




export const getBalance = createAsyncThunk(
  "/order/getBalance",
  async (value, thunkAPI) => {
    const token = thunkAPI.getState().auth.user.token;
    const newObj = {
      token: token,
    };
    let url = `http://localhost:3001/api/orders/getBalance`;
    const response = await axios.post(url, newObj);
    return response.data;
  }
);

const initialState = {
  value: null,
  error: null,
  balance: null,
  status: "idle",
  orderStatus: "idle",
};
export const ordersSlice = createSlice({
  name: "orders",
  initialState,
  reducers: {
    reset: (state) => initialState,
    resetStatus: (state) => {
        state.orderStatus = "idle";
    },
  },
  extraReducers(builder) {
    builder
      .addCase(MarketLongFunc.pending, (state, action) => {
        state.orderStatus = "loading";
      })
      .addCase(MarketLongFunc.fulfilled, (state, action) => {
        state.orderStatus = "success";
        // getBalance();
        // state.balance = action.payload;
      })
      .addCase(MarketLongFunc.rejected, (state, action) => {
        state.orderStatus = "failed";
        state.error = action.error.message;
      })
      .addCase(getBalance.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(getBalance.fulfilled, (state, action) => {
        // state.status = "success";
        state.balance = action.payload;
        state.status = "success";
      })
      .addCase(getBalance.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      });
  },
});

export const { reset } = ordersSlice.actions;
export default ordersSlice.reducer;

Now in my next component the useEffect will call if there is no balance yet and the user is logged in. The way in which I was trying to solve my issue was to use the state.orderStatus = "success" under MarketLongFunc.fulfilled, this way hypothetically I can dispatch getbalance under the useEffect if a MarketLong is placed and then change the status with reset like the following:

export const Orderform = () => {
  const user = useSelector((state) => state.auth.user);
  const balance = useSelector((state) => state.orders.balance);
  const status = useSelector((state) => state.orders.orderStatus);

  const dispatch = useDispatch();


  useEffect(() => {
    if (!balance && user) {
      dispatch(getBalance());
    }

    if (status == "success") {
      dispatch(getBalance());
      dispatch(resetStatus());
    }
  }, [balance]);

  if (user) {
    return (
      <div>
        <h1>
          cash balance: ${balance ? Math.round(balance.balance) : "error"}
        </h1>
        <MarketLong />
      </div>
    );
  }
  return (
    <div>
      Login
    </div>
  );
};

The above code does not work currently as when I console.log(status) on refresh is is idle and when I use marketLong it is loading but it never makes it to fulfilled so still the only way to update the balance that is displayed after an order is to refresh the page. I want to update the displayed balance without refreshing the page as refreshing the page will have to make two other API calls on top of the getBalance. I have left some comments in where I tried things like just putting the getBalance function inside the MarketLongFunc in the ordersSlice, I also tried returning it etc but that did nothing and I figured fixing this issue in the useEffect with the status' would be the best way to fix this but I am open to hearing other solutions besides creating redundant code where I just basically type out the getBalance func inside marketLongFunc.

Another way that almost works is just adding dispatch(getBalance()) after dispatch(MarketLongFunc(longItem)); in my MarketLong react component like the following:

  const addNewLong = async (e) => {
    e.preventDefault();

    const longItem = {
      userInput: req.ticker,
      quotePrice: req.quotePrice,
      quantity: Item.quantity,
    };

    dispatch(MarketLongFunc(longItem));
    dispatch(getBalance());

  };

The problem with this is the first order never gets updated but after that it updates incorrectly as the balance will be off by one buy order. I imagine this is due to getBalance gettting called before MarketLongFunc but without setting a manual setTimeout func which seems like a clunky solution, I am not sure how to fix that with redux, you would think something like : if (dispatch(MarketLongFunc(longItem))) {dispatch(getBalance())}, but maybe this way needs to be changed in the ordersSlice (which I had tried and was not able to get it to work).


Solution

  • There are many ways to solve this problem - I will describe an approximate solution:

    export const MarketLongFunc = createAsyncThunk(
      "/order/marketlong",
      async (value, thunkAPI) => {
        const token = thunkAPI.getState().auth.user.token;
        const newObj = {
          value: value,
          token: token,
        };
    
        let url = `http://localhost:3001/api/orders/marketlong`;
        const response = await axios.post(url, newObj);
        //getBalance() 
        return;
      }
    );
    
    
    
    
    export const getBalance = createAsyncThunk(
      "/order/getBalance",
      async (value, thunkAPI) => {
        const token = thunkAPI.getState().auth.user.token;
        const newObj = {
          token: token,
        };
        let url = `http://localhost:3001/api/orders/getBalance`;
        const response = await axios.post(url, newObj);
        return response.data;
      }
    );
    
    const initialState = {
      value: null,
      error: null,
      balance: null,
      status: "idle",
      orderStatus: "idle",
      balanceNeedsToBeUpdated: true // <--- HERE
    };
    export const ordersSlice = createSlice({
      name: "orders",
      initialState,
      reducers: {
        reset: (state) => initialState,
      },
      extraReducers(builder) {
        builder
          .addCase(MarketLongFunc.pending, (state, action) => {
            state.orderStatus = "loading";
          })
          .addCase(MarketLongFunc.fulfilled, (state, action) => {
            state.orderStatus = "idle";
            state.balanceNeedsToBeUpdated = true; // < ----- HERE
            // getBalance();
            // state.balance = action.payload;
          })
          .addCase(MarketLongFunc.rejected, (state, action) => {
            state.orderStatus = "failed";
            state.error = action.error.message;
          })
          .addCase(getBalance.pending, (state, action) => {
            state.status = "loading";
          })
          .addCase(getBalance.fulfilled, (state, action) => {
            // state.status = "success";
            state.balance = action.payload;
            state.status = "idle";
            state.balanceNeedsToBeUpdated = false; // <---- HERE
          })
          .addCase(getBalance.rejected, (state, action) => {
            state.status = "failed";
            state.error = action.error.message;
          });
      },
    });
    
    export const { reset } = ordersSlice.actions;
    export default ordersSlice.reducer;
    
    export const Orderform = () => {
      const user = useSelector((state) => state.auth.user);
      const balance = useSelector((state) => state.orders.balance);
      const status = useSelector((state) => state.orders.status);
      const orderStatus = useSelector((state) => state.orders.orderStatus);
      const balanceNeedsToBeUpdated = useSelector((state) => state.orders.balanceNeedsToBeUpdated);
    
      const dispatch = useDispatch();
    
    
      useEffect(() => {
        if (user && balanceNeedsToBeUpdated) { //< ----- HERE
          dispatch(getBalance());
        }
      }, [user, balanceNeedsToBeUpdated]); // < ---- HERE
    
      if (user) {
        if (status == 'loading' || orderStatus == 'loading') {
          return <div>loading</div>;
        }
        
        return (
          <div>
            <h1>
              cash balance: ${balance ? Math.round(balance.balance) : "error"}
            </h1>
            <MarketLong />
          </div>
        );
      }
      return (
        <div>
          Login
        </div>
      );
    };
    
    //....
    
      const addNewLong = async (e) => {
        e.preventDefault();
    
        const longItem = {
          userInput: req.ticker,
          quotePrice: req.quotePrice,
          quantity: Item.quantity,
        };
    
        dispatch(MarketLongFunc(longItem)); // < --- HERE
      };