react js react-router-dom stores not pass by providers - reactjs

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;
}
}

Related

How to use React Context API to have a state across multiple Routes?

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;

How to test components using Mobx stores with Jest

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.

React With Redux Thunk and Apollo Graphql - How to access client.query?

This is my app.js
import React from 'react'
import { Provider } from 'react-redux'
import { View } from 'react-native'
import { createStore, applyMiddleware } from 'redux'
import ReduxThunk from 'redux-thunk'
import Reducers from './redux'
import Routes from './config/routes'
import { ApolloClient, HttpLink, InMemoryCache } from 'apollo-boost'
import { ApolloProvider } from 'react-apollo'
const cache = new InMemoryCache();
const client = new ApolloClient({
cache,
link: new HttpLink({
uri: '...',
}),
})
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
initialized: true
}
}
render() {
return (
<View style={{ flex: 1 }}>
<ApolloProvider client={client}>
<Provider store={store}>
<Routes />
</Provider>
</ApolloProvider>
</View>
)
}
}
const store = createStore(Reducers, {}, applyMiddleware(ReduxThunk))
export default App
Ok, so far...basic.
This will render the initial file of my route: welcome.js
import React from 'react'
import {...} from 'react-native'
import { Actions } from 'react-native-router-flux'
import style from './style'
import { connect } from "react-redux"
import gql from 'graphql-tag'
class Welcome extends React.Component {
constructor(props) {
const LOGIN_MUTATION = gql`
mutation {
login(
email:"test#test.com"
password:"1234"
) {token}
}
`
// Bellow will not work..I've no idea how to call the client that
// I set at <ApolloProvider client={client}>
client
.mutate({
mutation: LOGIN_MUTATION
})
.then(res => console.log(res))
.catch(err => console.log(err))
}
}
const mapStateToProps = state => (
{
}
)
export default connect(mapStateToProps,
{
})(Welcome)
So, the client was defined on my app.js that's apply the provider and inject the routes.
I'd like to know how to be capable to execute the client defined at app,js into welcome.js
It's highly recommended that you switch for the new React Hooks version, using them, you can simply write const client = useApolloClient() to get access to your client instance:
import { useApolloClient } from '#apollo/react-hooks';
const Welcome = () => {
const client = useApolloClient();
return <h1>Welcome</h1>;
}
And regarding the ApolloProvider, it is configured in the same manner was you did, except that you can import it directly from the hooks package too, i.e import { ApolloProvider } from '#apollo/react-hooks -- and you can remove the react-apollo therefore.
See more details about hooks here: https://www.apollographql.com/docs/react/hooks-migration/.
But in case you really want to stay using class components, you can do:
import { getApolloContext } from 'react-apollo';
class Welcome extends React.Component {
...
}
Welcome.contextType = getApolloContext();
And then you'll be able to access the client using this.context.client inside your class:
class Welcome extends React.Component {
render() {
console.log('client', this.context.client);
return ...;
}
}
Welcome.contextType = getApolloContext();
You can use ApolloConsumer in your component to get access to the client:
To access the client directly, create an ApolloConsumer component and provide a render prop function as its child. The render prop function will be called with your ApolloClient instance as its only argument.
e.g.
import React from 'react';
import { ApolloConsumer } from "react-apollo";
const WithApolloClient = () => (
<ApolloConsumer>
{client => "We have access to the client!" /* do stuff here */}
</ApolloConsumer>
);

mobx with react store not reflected in observer

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;
}));
}

How to make Relay and React Routing work properly?

I am starting out with Relay and trying to make my routing work properly.
Unfortunately, I am not having much of luck.
Here is the error I am getting:
Uncaught TypeError: Component.getFragment is not a function
Here is the code I have for your reference:
index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import Relay from 'react-relay';
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
import {RelayRouter} from 'react-router-relay';
import App from './modules/app';
import Home from './modules/home';
const AppQueries = {
store: (Component) => Relay.QL `query {
store {
${Component.getFragment('store')}
}
}`
};
ReactDOM.render(
<RelayRouter history={browserHistory}>
<Route path='/' component={App} queries={AppQueries}>
<IndexRoute component={Home}/>
</Route>
</RelayRouter>,
document.getElementById('ch-root'));
app.jsx
import React, {Component} from 'react';
import Relay from 'react-relay';
import Header from './ui/header';
import Footer from './ui/footer';
class App extends Component {
render() {
return (
<div id="ch-container">
<Header/>
<section id="ch-body">
{this.props.children}
</section>
<Footer/>
</div>
);
}
}
App = Relay.createContainer(App, {
fragments: {
store: (Component) => Relay.QL `
fragment on Store {
${Component.getFragment('store')}
}
`
}
});
export default App;
home.jsx
import React, {Component} from 'react';
import Relay from 'react-relay';
import Loader from './ui/loader';
import AccountSelector from './account/account-selector';
const APP_HOST = window.CH_APP_HOST;
const CURR_HOST = `${window.location.protocol}//${window.location.host}`;
class Home extends Component {
state = {
activeAccount: null,
loading: true
}
render() {
const {activeAccount, loading} = this.state;
if (loading) {
return <Loader/>;
}
if (!activeAccount && !loading) {
return <AccountSelector/>;
}
return (
<h1>Hello!</h1>
);
}
}
Home = Relay.createContainer(Home, {
fragments: {
store: () => Relay.QL `
fragment on Store {
accounts {
unique_id,
subdomain
}
}
`
}
});
export default Home;
UPDATE
I made few changes suggested by freiksenet as below. But that raises the following two issues:
What would happen when I change the route and a component other than Home gets rendered by the App component?
I now get this error:
Warning: RelayContainer: Expected prop store to be supplied to
Home, but got undefined. Pass an explicit null if this is
intentional.
Here are the changes:
index.jsx
const AppQueries = {
store: () => Relay.QL `query {
store
}`
};
app.jsx
import Home from "./home";
...
App = Relay.createContainer(App, {
fragments: {
store: () => Relay.QL `
fragment on Store {
${Home.getFragment('store')}
}
`
}
});
Fragment definitions don't actually get Components as arguments, but a map of variables of the container, you only need to use them to have conditional fragments based on variable values.
Relay Route queries don't accept any arguments.
Here are the changes you need to make.
Route queries in Relay just need to specify the root query you are going to retrieve in this route, without the fragment.
index.jsx
const AppQueries = {
store: () => Relay.QL `query {
store
}`
};
Your App component actually doesn't use any relay data, so you can just export normal component instead of container.
export default class App extends Component {
If you'd need to pass some Relay data to it, you don't need to include child fragments, because fragments inclusion is only needed when you directly render other containers as direct children (not as this.props.children).
Then in Router you need to move the queries to Home.
ReactDOM.render(
<RelayRouter history={browserHistory}>
<Route path='/' component={App}>
<IndexRoute component={Home} queries={AppQueries} />
</Route>
</RelayRouter>,
document.getElementById('ch-root'));

Resources