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:
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 :(
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 .....
}