As with all things react I'm trying to do something simple and I'm guessing I'm missing some obvious configuration wise all I'm trying to do is take a redux app and implement mobx.
My issue is that I trying to go to the route /user/12345
The store is being called - I am getting data back from the API but I'm getting a few exceptions the first is from mobx
An uncaught exception occurred while calculation your computed value, autorun or tranformer. Or inside the render().... In 'User#.render()'
Then as is somewhat expected a null value is blowing up in the presenter because the store is not yet loaded
Uncaught TypeError: Cannot read property 'name' of null
Then in the store itself because of the returned api/promise where my user is being set a mobx exception
Uncaught(in promise) Error: [mobx] Invariant failed: It is not allowed to create or change state outside an `action`
I have added #action to the store function that is calling the api so I'm not sure what I've got messed up - and before I bubble gum and hodge podge a fix I would rather have some feedback how to do this correctly. Thanks.
UserStore.js
import userApi from '../api/userApi';
import {action, observable} from 'mobx';
class UserStore{
#observable User = null;
#action getUser(userId){
userApi.getUser(userId).then((data) => {
this.User = data;
});
}
}
const userStore = new UserStore();
export default userStore;
export{UserStore};
index.js
import {Router, browserHistory} from 'react-router';
import {useStrict} from 'mobx';
import routes from './routes-mob';
useStrict(true);
ReactDOM.render(
<Router history={browserHistory} routes={routes}></Router>,
document.getElementById('app')
);
routes-mob.js
import React from 'react';
import {Route,IndexRoute} from 'react-router';
import App from './components/MobApp';
import UserDetail from './components/userdetail';
export default(
<Route name="root" path="/" component={App}>
<Route name="user" path="user/:id" component={UserDetail} />
</Route>
);
MobApp.js
import React,{ Component } from 'react';
import UserStore from '../mob-stores/UserStore';
export default class App extends Component{
static contextTypes = {
router: React.PropTypes.object.isRequired
};
static childContextTypes = {
store: React.PropTypes.object
};
getChildContext(){
return {
store: {
user: UserStore
}
}
}
render(){
return this.props.children;
}
}
.component/userdetail/index.js (container?)
import React, {Component} from 'react';
import userStore from '../../mob-stores/UserStore';
import User from './presenter';
class UserContainer extends Component{
static childContextTypes = {
store: React.PropTypes.object
};
getChildContext(){
return {store: {user: userStore}}
}
componentWillMount(){
if(this.props.params.id){
userStore.getUser(this.props.params.id);
}
}
render(){
return(<User />)
}
}
export default UserContainer;
.component/userdetail/presenter.js
import React, {Component} from 'react';
import {observer} from 'mobx-react';
#observer
class User extends Component{
static contextTypes = {
store: React.PropTypes.object.isRequired
}
render(){
const {user} = this.context.store;
return(
<div>{user.User.name}</div>
)
}
}
Forgive me if its a little messy its what I've pieced together for how to implement mobx from various blog posts and the documentation and stack overflow questions. I've had a hard time finding a soup-to-nuts example that is not just the standard todoapp
UPDATE
Basically the fix is a combination of #mweststrate answer below adding the action to the promise response
#action getUser(userId){
userApi.getUser(userId).then(action("optional name", (data) => {
// not in action
this.User = data;
}));
}
and including a check in the presenter that we actually have something to display
<div>{this.context.store.user.User ? this.context.store.user.User.name : 'nada' }</div>
Note that the following code is not protected by an action:
#action getUser(userId){
userApi.getUser(userId).then((data) => {
// not in action
this.User = data;
});
}
The action only decorates the current function, but the callback is a nother function, so instead use:
#action getUser(userId){
userApi.getUser(userId).then(action("optional name", (data) => {
// not in action
this.User = data;
}));
}
Related
I have a problem to access this.context in a class based consumer component. I have the following situation:
AppContext.js:
import React from "react";
const ContactContext = React.createContext(); // Create our context
export default ContactContext;
DataProvider.js:
import React, { Fragment } from "react";
import AppContext from "./AppContext";
export default class DataProvider extends React.Component {
state = {
contacts: {
contact1: {
id: 1,
firstName: 'Test User FN',
lastName: 'Test User LN'
}
}
};
render() {
return (
<>
<AppContext.Provider value={{contacts: this.state.contacts}}>
{this.props.children}
</AppContext.Provider>
</>
);
}
}
App.js:
import React from 'react';
import DataProvider from "./DataProvider";
import Contact from './components/contact/contact.component';
export default class App extends React.Component {
render() {
return (
<div>
<div className="container">
<DataProvider>
<Contact contactIndex={0}/>
</DataProvider>
</div>
</div>
);
}
}
The consumer Contact.js:
import React, { Component } from "react";
import AppContext from '../context/AppContext'
export default class Contact extends Component {
static contextType = AppContext;
componentDidMount () {
console.log('My context is: ' + this.context);
}
render() {
return (
<div className="card"></div>
);
}
}
The console output is:
My context is: undefined
Thanks for any help
Regards
Dakir
Only difference I see in the other answer's CodeSandbox is the import path is different.
import AppContext from "./AppContext";
vs:
import AppContext from '../context/AppContext'
Maybe OP has a filepath/import error?
p.s. If this is the error, TypeScript is a lifesaver for avoiding these kind of things in JS.
Your code seems right to me, I tried to replicate it in a Sandbox to find out the error and somehow works like a charm.
https://codesandbox.io/s/interesting-https-emgoz?file=/src/App.js
Tried to spot the difference but I couldn't honestly.
I'm trying to understand how the context API works. I'd like to keep a global state that I can update from any Class Component. Currently, when I try to update my Context using the provided function, It only updates the value locally. In my code, I try to update a field "Day" into "Hello", and the change can be seen only when Writer is rendered. As soon as I ask my browser to render "Reader", the value is "Day" again. Why does this happen? Here's my code, I simplified it as much as I could:
index.js:
import React from "react";
import ReactDOM from "react-dom";
import {ThemeContextProvider} from "./ThemeContext";
import App from "./App";
ReactDOM.render(
<ThemeContextProvider>
<App />
</ThemeContextProvider>,
document.getElementById("root")
);
app.js:
import React from "react";
import Writer from "./Writer.js";
import Reader from "./Reader.js";
import { Context } from "./ThemeContext.js";
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
class App extends React.Component {
static contextType = Context;
constructor(props) {
super(props);
}
render() {
return (
<div className="app">
<Router>
<Switch>
<Route path="/writer" component={Writer}></Route>
<Route path="/reader" component={Reader}></Route>
</Switch>
</Router>
</div>
);
}
}
export default App;
context.js:
import React, { Component } from "react";
const Context = React.createContext();
const { Provider, Consumer } = Context
// Note: You could also use hooks to provide state and convert this into a functional component.
class ThemeContextProvider extends Component {
state = {
theme: "Day"
};
setTheme = (newTheme) => {
this.setState({theme: newTheme})
};
render() {
return <Provider value={{theme: this.state.theme, setTheme: this.setTheme}}>{this.props.children}</Provider>;
}
}
export { ThemeContextProvider, Consumer as ThemeContextConsumer, Context };
writer.js:
import React from "react";
import {Context} from "./ThemeContext";
class Writer extends React.Component {
static contextType = Context
constructor(props) {
super(props);
this.write = this.write.bind(this)
}
write () {
this.context.setTheme("hello")
}
render() {
return (
<div>
<button onClick={this.write}>press</button>
<p>{this.context.theme}</p>
</div>
);
}
}
export default Writer;
reader.js:
import React from "react";
import { Context, ThemeContextConsumer } from "./ThemeContext";
class Reader extends React.Component {
static contextType = Context;
constructor(props) {
super(props);
}
render () {
return(
<div>
<p>{this.context.theme}</p>
</div>
);}
}
export default Reader;
how do you handle the maneuver to different pages? If right now, you handle it manually by typing it directly in the search top browser input placeholder. Then it will not work since the page getting refresh. Using just context api will not make your data persistant. You need to incorporate the use of some kind of storage to make it persistant.
Anyhow, your code should work if there's not page refresh happen. To see it in different pages tho, you can and a Link (from react-router-dom package) or basically a button to redirect you to different pages, like so:-
just add this in your Writer.js component for testing purposes:-
import React from "react";
import { Link } from 'react-router-dom'
import {Context} from "./ThemeContext";
class Writer extends React.Component {
static contextType = Context
constructor(props) {
super(props);
this.write = this.write.bind(this)
}
write () {
this.context.setTheme("hello")
}
render() {
return (
<div>
<button onClick={this.write}>press</button>
<p>{this.context.theme}</p>
<Link to="/reader">Go to Reader page</Link>
</div>
);
}
}
export default Writer;
I'm trying to test my React components using Mobx stores with Jest and React-testing-library.
The problem is that I have no clues on how to inject my stores for the test.
Here is my simplified codes.
StaffInfo.js(component)
import React, { useState } from "react";
import { observer, inject } from "mobx-react";
const StaffInfo = props => {
const store = props.instituteStore;
const [staffs, setStaffs] = useState(store.staffs);
return (
<div>
....
</div>
);
}
export default inject(rootStore => ({
instituteStore : rootStore.instituteStore
}))(observer(StaffInfo));
index.js(Root store)
import LoginStore from "./LoginStore";
import InstituteStore from "./InstituteStore";
class RootStore {
constructor(){
this.loginStore = new LoginStore (this);
this.instituteStore = new InstituteStore(this);
}
}
export default RootStore;
InstituteStore.js(target store)
import { observable, action } from "mobx";
class InstituteStore {
constructor(root){
this.root = root;
}
#observable
staffs = [];
}
export default InstituteStore;
StaffInfo.test.js(test file)
import React from "react";
import ReactDom from "react-dom";
import { MemoryRouter } from "react-router-dom";
import { Provider } from "mobx-react";
import StaffInfo from "./StaffInfo";
import InstituteStore from "../stores/InstituteStore";
describe("Staff Component testing", () => {
test("should be rendered without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(
<MemoryRouter initialEntries={["/staff"]}>
<StaffInfo instituteStore={RootStore.instituteStore} />
</MemoryRouter>,
div
);
ReactDOM.unmountComponentAtNode(div);
});
});
As soon as running this test file, the error messages are like :
TypeError : Cannot read property 'staffs' of undefined
Please tell me which parts of the codes are wrong.
Thanks so much in advance!
Mobx-react's Inject is used to insert stores to the deep child component. These stars are provided by the context-based API Provider.
so wherever you are providing the stores to the child components use something like.
import rootStore from 'path_to_rootStore'
<Provider rootStore={rootStore}>
...
...
<App/>
...
...
<.Provider>
Thanks to #uneet7:
Legend! Finally someone gave a sensible answer :D
This is what My component looks like and
#inject('routing', 'navigationStore')
#observer
export default class PageTitle extends React.Component<*> {...}
And this is how I made it work:
let view = mount(
<Provider {...getStores()}>
<UserPage notificationStore={notificationStore} routing={routing} />
</Provider>
);
So the UserPage has components (many) and one of those components has PageTitle component. Obviously PageTitle has the #inject on it. It doesn't matter, as Provider HOC will provide stores via inject function to the component props.
I'm using react js with mobx and I'm trying to pass stores in providers and use it but,it seems It's not pass by the providers and I don't have access to it.
in addition when I'm trying to inject the UserStore, the web app is failed and throw an error that UserStore is not available
import { Switch, Route} from 'react-router-dom';
import React, {Component} from 'react';
import {Router} from 'react-router-dom';
import createBrowserHistory from 'history/createBrowserHistory';
import {Provider} from 'mobx-react'
import { TodoStore,UserStore, ModalsStore} from '../stores'
import App from './App';
import {Login} from '../screens'
const stores = { UserStore}
const browserHistory = createBrowserHistory();
export default class Root extends Component {
render() {
return (
<Provider stores={stores}>
<Router history={browserHistory}>
<Switch>
<Route exact path='/login' component={Login}/>
<Route component={App}/>
</Switch>
</Router>
</Provider>
)
}
}
piece of my App component
#observer
export default class App extends Component {
constructor(props){
super(props);
console.log('appProps',props)
}
render() {
...........
}
UserStore
import {observable,action} from 'mobx'
class UserStore {
#observable token = false
#observable first_name = '';
#observable last_name = ''
#action setUser(data) {
this.token = data.token;
this.first_name = data.first_name;
this.last_name = data.last_name;
}
#action updateUser(data) {
this.first_name = data.first_name;
this.last_name = data.last_name;
}
#action setToken(token){
this.token = token;
}
}
const singelton = new UserStore()
export default singelton
I'm trying to use the userStore and have access but in console i get
You have to #inject('stores') in your App class.
Like this:
import React, { Component } from 'react';
import { observer, inject } from 'mobx-react';
#inject('stores')
#observer
export default class App extends Component {
render() {
console.log(this.props.stores);
return (
<div>{ /* your components */}</div>
);
}
}
Basically for every class, if you want the store in the props, you have to use inject.
Personally, I prefer import stores from './UserStore' without Provider and inject.
In this way, you can access the store directly, and set any observable inside store the same way as setState.
The code below is the MobX way to use singleton store with observer and observable without using setState():
import React, { Component } from 'react';
import { observer } from 'mobx-react';
import stores from './userStore';
#observer
export default class App extends Component {
render() {
return (
<div>
<input value={stores.first_name} onChange={this.onChangeHandler}/>
</div>
);
}
onChangeHandler = e => {
// MobX will setState and trigger the React re-render for you
stores.first_name = e.target.value;
}
}
I have simple component that uses mobx and decorators like this
import * as React from "react";
import { observer, inject } from "mobx-react/native";
import { Router as ReactRouter, Switch, Route } from "react-router-native";
import Dashboard from "./_dashboard";
import { RouterInterface } from "../store/_router";
// -- types ----------------------------------------------------------------- //
export interface Props {
router: RouterInterface;
}
#inject("router")
#observer
class Router extends React.Component<Props, {}> {
// -- render -------------------------------------------------------------- //
render() {
const { router } = this.props;
return (
<ReactRouter history={router.history}>
<Switch location={router.location}>
<Route path="/" component={Dashboard} />
</Switch>
</ReactRouter>
);
}
}
export default Router;
essentially #inject("router") adds this.props.router that satisfies Props interface above, however typescript doesn't account for this and whenever I use this component somewhere I get an error if I don't pass down router in props, hence I need to change to router?: RouterInterface; which is fine, but not ideal.
Is there a way to fix this issue where typescript accounts for decorators injecting the props?
There is a way around it.
You can declare your injected props in a separate interface and then write a getter function. I wrote about it here:
https://gist.github.com/JulianG/18af9b9ff582764a87639d61d4587da1#a-slightly-better-solution
interface InjectedProps {
bananaStore: BananaStore; // 👍 no question mark here
}
interface BananaProps {
label: string;
}
class BananaComponent extends Component<BananaProps> {
get injected(): InjectedProps {
return this.props as BananaProps & InjectedProps;
}
render() {
const bananas = this.injected.bananaStore.bananas; // 👍 no exclamation mark here
return <p>{this.props.label}:{bananas}</p>
}
}