reactjsstatemobxmobx-react

React, Mobx. Looped component render due to state change


Before rendering, my component should find out whether user authorized.

Function checkAuth( ) do this task. While function does its work, state isLoading equal true. Before the end of its work, the function assigns state isLoading to false

While this state equal true, component renders loading animation.

When state equal false, component renders another content

My problem is that the component is looping to render over and over again. If I understand correctly, the state changes in the function checkAuth(), and component as observer reacts to this changing and re-renders itself. After rerender, the function checkAuth() starts again and so on in a circle.

I tried call this function inside useEffect and outside of this. Nothing helped me.

I expecting: function checkAuth() runs state isLodaing assigns to true component renders checkAuth continues to do its job before end of function checkAuth, state isLoading assigns to false

component observes this stop rendering start rendering another content: routes

Thank you very much for reading.

Component:

const AppRouter: FC = () => {
  const controller = useContext(ControllerContext)

  useEffect(() => {
    controller.checkAuth()

  })

  if (controller.appStore.getIsLoading) {
    console.log('loading');

    return (<Loading />);
  }

  else return (
    <div>routes</div>

    // <Routes>

    //   {/* Private routes for authorized user */}
    //   <Route element={<PrivateRoutes />}>
    //     <Route element={<MainPageView />} path="/" />
    //     <Route element={<SettingsPage />} path="/settings" />
    //   </Route>

    //   {/* Public routes for unauthorized users */}
    //   <Route element={<UnauthorizedRoutes />}>
    //     <Route element={<LoginPage />} path="/login" />
    //     <Route element={<SignupPage />} path="/signup" />
    //   </Route>

    //   <Route element={<ErrorPage />} path="*" />

    // </Routes>
  )
};

export default observer(AppRouter);

Function checkAuth():

async checkAuth() {
        this.appStore.setLoading(true)
        try {

            this.userStore.setUser({
                nickname: 'igor',
                email: 'igorasdasda',
                id: 1,
                avatar: '',
                about: '',
                isOnline: true,
                onlineTime: ''
            });

            this.appStore.setAuth(true)
        }
        catch (e: any) {
            console.log(e.response.data)
        }
        finally {
            this.appStore.setLoading(false)
            console.log(this.userStore.getUser);
        }
    }

My state stores:

export class UserStore {
    constructor() { makeObservable(this) }

    @observable private user = {} as IUser

    @computed get getUser() {
        return this.user
    }
    
    @action  setUser(newUser: IUser) {
        this.user = newUser
    }
}
export class AppStore {
    constructor() { makeObservable(this) }

    // Loading state
    @observable private isLoading = false

    @computed get getIsLoading() {
        return this.isLoading
    }
    @action setLoading(value: boolean) {
        this.isLoading = value
    }

    // Auth state
    @observable private isAuth = false;

    @computed get getIsAuth() {
        return this.isAuth
    }
    @action setAuth(value: boolean) {
        this.isAuth = value
    }
}

Solution

  • Your useEffect should have the controller as the only dependency. like this:

      useEffect(() => {
        controller.checkAuth();
      }, [controller]);
    

    With no dependency array (Like you did), the useEffect will trigger every render, which will cause another checkAuth, and you don't want that. Having the controller as your only dependency means that only if the reference of the controller changes (Which will not happen) the useEffect will run again.

    Few more comments about your code:

    1. In your stores, you don't have to create a private observable, a getter and a setter, like you did with isLoading. it's redundant. you just make the the field public without the setter and the getter.

    2. I would avoid export default

    3. It's better to put the observer on the function itself, while naming it (Better for the react dev tools), like this: const AppRouter: FC = observer(function AppRouter() {