Search code examples
javascripttypescriptmongodbrealmmobx-state-tree

Trying to connect to MongoDB using Realm


It's the first time I use Realm and MongoDB. I followed this good tutorial as starting point and I create this project.

https://codesandbox.io/s/realm-forked-mrjex?file=/src/state/DbModel.ts

The folder structure is:

src
|_ components
   |_ pages
      |_ Authentication.tsx
      |_ Home.tsx
      |_ Logout.tsx
   |_ App.tsx
   |_ Navigation.tsx
   |_ RestaurantCard.tsx
|_ lib
   |_ db-utils.ts
|_ state
   |_ index.ts
   |_ DbModel.ts

They are all very easy components, I post here only some pieces of them.

App.tsx:

const serviceName = "mongodb-atlas";

export function App() {
  return (
    <Provider value={stateInstance}>
      <AppWithState />
    </Provider>
  );
}

function AppWithState() {
  const {
    db: { app, client, setClient, user, setUser }
  } = useMst();

  useEffect(() => {
    async function init() {
      if (!user) {
        const credentials = Realm.Credentials.anonymous();
        const newUser = app.currentUser
          ? app.currentUser
          : await app.logIn(credentials);
        setUser(newUser);
      }
      if (!client) {
        const newClient = app.currentUser.mongoClient(serviceName);
        setClient(newClient);
      }
    }
    init();
  }, [app, client, user]);

  return (
    <Router>
      <Navigation />
      <Switch>
        <Route path="/" component={Home} />
        ...
      </Switch>
    </Router>
  );
}

state/index.ts:

export const StateModel = t
  .model("StateModel", {
    db: t.optional(DbModel, {} as DbModelInstance)
  })
  .views((self) => ({}))
  .actions((self) => ({}));

export const stateInstance = StateModel.create();
export interface StateInstance extends Instance<typeof StateModel> {}

const RootStateContext = createContext<StateInstance | null>(null);
export const Provider = RootStateContext.Provider;

export function useMst() {
  const state = useContext(RootStateContext);
  if (state === null)
    throw new Error("State cannot be null, please add a context provider");
  return state;
}

state/DbModel.ts:

const appId = process.env.REACT_APP_REALM_APP_ID;
const appConfig: Realm.AppConfiguration = {
  id: appId
};
const app: Realm.App = new Realm.App(appConfig);

export const DbModel = t
  .model("DbModel", {
    app: t.optional(t.frozen<Realm.App>(), app),
    user: t.optional(t.frozen<Realm.User>(), null),
    client: t.optional(t.frozen<any>(), null)
  })
  .views((self) => ({
    get root() {
      return getRoot(self) as any;
    }
  }))
  .views((self) => ({}))
  .actions((self) => ({
    setApp(app: Realm.App) {
      self.app = app;
    },

    setUser(user: Realm.User) {
      self.user = user;
    },

    setClient(client: any) {
      self.client = client;
    }
  }))
  .actions((self) => ({}));

export interface DbModelInstance extends Instance<typeof DbModel> {}

Home.tsx:

export function Home() {
  const {
    db: { user, client }
  } = useMst();

  const [restaurants, setRestaurants] = useState([]);
  const isLoading = restaurants.length === 0;

  useEffect(() => {
    async function getData() {
      if (!client || !user) return;
      const rests = client.db("sample_restaurants").collection("restaurants");
      setRestaurants(await rests.find());
    }

    getData();
  }, [isLoading, client, user]);

  if (isLoading) {
    return <div>HOME Loading...</div>;
  }

  return (
    <div>
      {restaurants.map((restaurant) => (
        <RestaurantCard key={restaurant._id} restaurant={restaurant} />
      ))}
    </div>
  );
}

Authentication.tsx:

const userSchema = yup.object().shape({
  email: yup.string().email().required(),
  password: yup.string().required().min(8)
});

export function Authentication({ type = "login" }) {
  const {
    db: { app, user, setUser, client }
  } = useMst();

  const [isLoading, setIsLoading] = useState(false);
  const history = useHistory();

  useEffect(() => {
    if (!isAnon(user)) {
      history.push("/");
    }
  }, [history, user]);

  async function submitHandler(values: any) {
    setIsLoading(true);
    if (type === "create") {
      // @ts-ignore
      await app.emailPasswordAuth.registerUser(values.email, values.password);
    }
    // login user and redirect to home
    const credentials = Realm.Credentials.emailPassword(
      values.email,
      values.password
    );
    // @ts-ignore
    setUser(await app.logIn(credentials));
    setIsLoading(false);
  }

  return (
    <Formik
      initialValues={{
        email: "",
        password: ""
      }}
      validationSchema={userSchema}
      onSubmit={submitHandler}
    >
      {({ errors, touched, handleSubmit, values, handleChange }) => (
        <Form noValidate onSubmit={handleSubmit}>
          {isLoading && <div className="">AUTH Loading...</div>}

          <div>
            <h1>{type === "login" ? "Login" : "Sign Up"}</h1>
            <Form.Row>
              <Form.Label>Email</Form.Label>
              <Form.Control
                type="email"
                name="email"
                value={values.email}
                onChange={handleChange}
                isValid={touched.email && !errors.email}
              />
              <Form.Control.Feedback>{errors.email}</Form.Control.Feedback>
            </Form.Row>
            <Form.Row>
              <Form.Label>Password</Form.Label>
              <Form.Control
                type="password"
                name="password"
                value={values.password}
                onChange={handleChange}
                isValid={touched.password && !errors.password}
              />
              <Form.Control.Feedback>{errors.password}</Form.Control.Feedback>
            </Form.Row>

            <div className="text-center mt-2">
              <Button variant="primary" type="submit">
                Submit
              </Button>
            </div>
          </div>
        </Form>
      )}
    </Formik>
  );
}

function isAnon(user: Realm.User) {
  return !user || user.identities[0].providerType === "anon-user";
}

Basically I used a sample DB of restaurants. In the tutorial, the author use the React context to save Db info like app, user and client but I prefer to setup a Mobx state tree. I think this is the only difference. Oh, and I use TypeScript (btw, what is the type of client? I didn't understand reading the guide, it seems MongoDB but from where need I to import it?).

My code doesn't work. I get nothing, still loading:

enter image description here

My app I think is stuck in Home component, in the getData() function because both client and user are null, but in App I created them and saved in my state so I don't understand what's going wrong..

EDIT: and sometimes, as Danila noted, I get also this error Cannot assign to read only property '_locationUrl' of object '#<App>'.

I cloned the repo created by the author, it works. What's wrong with my code? I think is a problem of a Promise but I'm not sure and I don't know how to solve :(


Solution

  • You are using mobx-state-tree with types.frozen in DbModel.ts. That is messing with Realm.App because internally MongoDB Realm code is trying to alter the Realm.App instance but it will fail since you have that instance frozen.

    Moving the Realm.App creation in your App code should fix the issue. Something like:

    function AppWithState() {
      const {
        db: {   client, setClient, user, setUser }
      } = useMst();
      const [app] = useState(new Realm.App(appConfig)) // <-- here is the fix, along with deleting the Realm.App instantiation code from DbModel.ts
     
      .... rest of your AppWithState code .....
    }