MobX observables change do not always trigger observer components render - reactjs

My project is built on React, and for state management Mobx is being used.
We are not using the decorators, so the components that need to observe the observables, need to be wrapped in the following way:
import React from 'react';
import {observer} from 'mobx-react';
import MyComponent from '../app/my-component.jsx';
import myFilter from './my-filters.jsx';
export default observer(() => {
return <MyComponent filter={myFilter}/>;
});
The component MyComponent is receiving the observables as props:
static propTypes() {
return {
myFilter: React.PropTypes.function.isRequired
};
}
And it is used in the render method:
render() {
if (this.props.myFilter.filterValue1 !== null) {
// some code here
}
}
myFilter in this case is the observable, which looks somehow like this:
import {observable} from 'mobx';
const myFilter = observable({
filterValue1: null,
filterValue2: null,
addOrRemoveItem() {
// some function here
}
});
export default myFilter;
In this case, if some component alters myFilter, the observer MyComponent which receives the observable as props, does not always re-render. In some cases this can be solved by addressing the observable object by attribute before the call of the component. E.g.:
export default observer(() => {
console.log(myFilter.filterValue1);
return <MyComponent filter={myFilter}/>;
});
But this is not stable.
Is there a valid workaround to avoid this?

Declare MyComponent as an observer.
export in app/my-component.jsx should look like this
export default observer(MyComponent);
This piece of code
export default observer(() => {
return <MyComponent filter={myFilter}/>;
});
turns an anonymous stateless component, not MyComponent, into an observer.
MobX documentation clearly articulates that you need to apply observer to all components that render observable data or you will encounter problems.
The other way to solve the problem would be passing plain data from this anonymous stateless component into MyComponent.
export default observer(() => {
return <MyComponent filterValue1={myFilter.filterValue1}/>;
});
This way MyComponent will be rerendered because it receives new props each time its parent is rerendered.

Alik is right, you need MyComponent to be an observer as well.
Otherwise, your code <MyComponent filter={myFilter}/> inside the only observable you have means that you want refresh only when myFilter object (ie., reference to it) changes. It does not access any of its properties and thus refresh is not needed when those properties change. That's why your component was refreshed when you accessed the filterValue1 property in the console.log statement.

Related

How to render a component with props derivative from NextJS router

I'm trying to render a component that uses a dynamic router path prop. I want mysite.com/something to load the component with the something prop. If the route is mysite.com/somethingelse, I want to load the component with the somethingelse prop. Here's my code:
page.js:
import { useRouter } from "next/router";
import List from "./List";
function DefaultPage() {
const router = useRouter();
console.log(router.query.category); // Works correctly
return (
<div>
<List category={router.query.category} />
</div>
);
}
export default DefaultPage;
The component, list.js:
import React, { Component } from "react";
class List extends Component {
constructor(props) {
super(props);
console.log(this.props.category); // This is where I'm confused
}
static defaultProps = { category: "default" };
render() {
return <p>Hello</p>;
}
}
export default List;
The problem is, this.props.category always returns as default (my default prop), unless I recompile. It works perfectly after a fresh compile, but then breaks after every subsequent refresh in the browser.
I can visually see the router query returning the correct value in the log, but the component is rendering before everything else, thus returning a default value. Is there a way I can stop the List component from rendering before its own props are specified? Or is there a better way of doing this all together? Thanks.
I would do something like this in the DefaultPage component:
if(router.query.category === 'something') {
return <ListComponent/>
}
if(router.query.category === 'somethingElse') {
return <SomethingElseComponent/>
}
If you don't want to use two separate components, you could pass the prop to useEffect so it can re-render the component when that prop changes https://reactjs.org/docs/hooks-effect.html

Difference Between Class.contextType and Context.Consumer with working example

I am trying to understand the React context API and was going through the official docs. I will appreciate if someone can throw some more light on the following points as the official doc does not address it clearly.
What is the difference in contextType and Consumer methods to
consume the values provided by Provider? In what situation we should
use which method?
Can the value exposed by Provider in a class based component, be
used by a react hook component using useContext? I had the same
setup and i ended up converting the useContext to Context.Consumer.
I have a very straightforward setup in which i have a Provider Class
based component which is exposing some state values. The Provider
has only one children component which is also a consumer. When i use
Context.Consumer in the children to fetch the values, everything
works as expected. But when i use contextType in the children
component, i see an empty object.
ContextProvider.js
import React from "react";
import {ContextConsumer} from "./ContextConsumer";
export const TestContext = React.createContext({
count: 1,
incrCount: (count)=>{
console.log(`count value :- ${count}`)
}
});
export class ContextProvider extends React.Component {
incrCount = () => {
this.setState({
count: this.state.count + 1,
});
};
state = {
count: 5,
incrCount: this.incrCount,
};
render() {
return (
<TestContext.Provider value={this.state}>
<ContextConsumer />
</TestContext.Provider>
);
}
}
ContextConsumer.js
import React from "react";
import { TestContext } from "./ContextProvider";
export class ContextConsumer extends React.Component {
static contextType=TestContext
componentDidMount() {
const {count,incrCount}= this.context;
console.log(`count:- ${(count)}`)
console.log(`incrCount:- ${incrCount}`)
}
render() {
return (
<div>
**// BELOW CODE IS WORKING AS EXPECTED**
<TestContext.Consumer>
{({ count, incrCount }) => (
<button onClick={incrCount}>Count is {count}</button>
)}
</TestContext.Consumer>
</div>
);
}
}
App.js
import {ContextProvider} from "../../playground/ContextProvider";
const output = (
<Provider store={reduxStore}>
<ContextProvider />
</Provider>
);
ReactDOM.render(output, document.getElementById("root"));
What is the difference in contextType and Consumer methods to consume the values provided by Provider? In what situation we should use which method?
The static contextType assignment was introduced in v16.6.0 as a way to use context outside of render method. The only difference between Consumer and static context is the fact that using contextType allows you use context outside of render method too
Can the value exposed by Provider in a class based component, be used by a react hook component using useContext?
Yes the context value from Provider can be used by useContext too. However you can only make use of useContext inside a functional component and not a class component and also after v16.8.0 or react which supports hooks
P.S. You must ensure one thing that you are not causing a circular dependency by importing provider in consumer component and also the other way around
Static contextType and class.contextType
useContext
context.Consumer
are almost same the difference between them is that (1) is used in class component and
useContext is a hook and the best thing is we can use this hook multiple times in one functional component.(3) can only be used in jsx or in render(return).(1)and(2) can be used outside return.
to put it more simply:
if (functional components) { useContext }
else {
if (multiple contexts) { Context.Consumer }
else { Class.contextType }
}
// "static contextType" is experimental lets avoid

React/Mobx: Integration Tests with Stores Injected into Child Components

We are trying to write out unit/integration tests for all of our existing React components. We are currently using React with Mobx 4, with tests written mostly with react-testing-library/jest. We did use Enzyme in some areas as well to make use of shallow rendering.
Our issue is that as we get to some of our 'pages', or container components, we are getting errors such as "MobX injector: Store 'teamStore' is not available! Make sure it is provided by some Provider"
We've done a bit of digging but couldn't find anything in our searches of similar issues for reference. We do know that this is caused by child components that have stores injected into them directly, and that are called into our container/page.
My question is: Is there any way within the testing frameworks to pass down mock stores created in our container components down to child components? Obviously if we passed the store as a prop from the parent to the child, that solves the issue, but we are trying to avoid modifying the components themselves in any way.
If the above is not possible, do we have any other options without refactoring components to pass down stores as needed rather than injecting directly into child components?
import React, { Component } from "react";
import { inject, observer } from "mobx-react";
import { Container, Grid, Segment } from "semantic-ui-react";
import ChildComp from "../../components/ChildComp";
#inject("userStore")
#observer
class ParentComponent extends Component {
render() {
return (
<Container className="parent">
<Segment basic>
<h1>Hello</h1>
<ChildComp />
</Segment>
</Container>
);
}
}
export default ParentComponent;
import React, { Component } from "react";
import { inject, observer } from "mobx-react";
import { Container, Grid, Segment } from "semantic-ui-react";
#inject("teamStore")
#observer
class ChildComp extends Component {
render() {
return (
<Segment basic>
<p>How can I help you?</p>
</Segment>
);
}
}
export default ChildComp;
Using jest you can mock parts of mobx to provide your own mock store so instead of running the real inject function you can provide your own inject function instead.
Using that custom inject function you can return a fake store (which needs to match the same interface as the original store).
If you want to pre populate the store with values by importing the mock you created (jest doesn't allow variables on the module/global scope to be used when using jest.mock)
Here is an example code that achieves this (this is untested code written right here on stackoverflow so might needs some tweaks to get right).
jest.mock('mobx-react', () => {
// get the original reference to mobx-react
const originalMobx = require.requireActual('mobx-react');
// create your fake stores, they should have the same interface as the real store
const mockStores = {
userStore: new UserStore()
};
return {
...originalMobx, // allow to import the original properties in react-mobx
// override the inject decorator to instead return the fake store as a prop
inject: (injectName) => (component) => (props) => {
// render the real component with the additional prop
return react.createElement(component, {...props, [injectName]: mockStores[injectName] })
},
mockStores // Allows access afterwards via import e.g import { mockStores } from 'mobx-react'
}
});
Once you mocked the mobx-react inject function you can reference the store to pre populate the values by:
import { mockStores } from 'mobx-react';
test('my test', () => {
mockStores.userStore.clearUsers();
// render the component here
})
There is also an alternative solution where you can just wrap the tested component with Provider from mobx-react and supply fake stores.
so the test will initialize them beforehand and pass the down the context.
e.g
test('my comp', () => {
const userStore = new UserStore();
const component = shallow(
<Provider userStore={userStore}>
<MyComponent />
</Provider>
)
});

Nested components testing with Enzyme inside of React & Redux

I have a component SampleComponent that mounts another "connected component" (i.e. container). When I try to test SampleComponent by mounting (since I need the componentDidMount), I get the error:
Invariant Violation: Could not find "store" in either the context or
props of "Connect(ContainerComponent)". Either wrap the root component
in a , or explicitly pass "store" as a prop to
"Connect(ContainerComponent)".
What's the best way of testing this?
Enzyme's mount takes optional parameters. The two that are necessary for what you need are
options.context: (Object [optional]): Context to be passed into the component
options.childContextTypes: (Object [optional]): Merged contextTypes for all children of the wrapper
You would mount SampleComponent with an options object like so:
const store = {
subscribe: () => {},
dispatch: () => {},
getState: () => ({ ... whatever state you need to pass in ... })
}
const options = {
context: { store },
childContextTypes: { store: React.PropTypes.object.isRequired }
}
const _wrapper = mount(<SampleComponent {...defaultProps} />, options)
Now your SampleComponent will pass the context you provided down to the connected component.
What I essentially did was bring in my redux store (and Provider) and wrapped it in a utility component as follows:
export const CustomProvider = ({ children }) => {
return (
<Provider store={store}>
{children}
</Provider>
);
};
then, I mount the SampleComponent and run tests against it:
it('contains <ChildComponent/> Component', () => {
const wrapper = mount(
<CustomProvider>
<SampleComponent {...defaultProps} />
</CustomProvider>
);
expect(wrapper.find(ChildComponent)).to.have.length(1);
});
Option 1)
You can wrap the container component with React-Redux's Provider component within your test. So with this approach, you actually reference the store, pass it to the Provider, and compose your component under test inside. The advantage of this approach is you can actually create a custom store for the test. This approach is useful if you want to test the Redux-related portions of your component.
Option 2)
Maybe you don't care about testing the Redux-related pieces. If you're merely interested in testing the component's rendering and local state-related behaviors, you can simply add a named export for the unconnected plain version of your component. And just to clarify when you add the "export" keyword to your class basically you are saying that now the class could be imported in 2 ways either with curly braces {} or not. example:
export class MyComponent extends React.Component{ render(){ ... }}
...
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
later on your test file:
import MyComponent from 'your-path/MyComponent'; // it needs a store because you use "default export" with connect
import {MyComponent} from 'your-path/MyComponent'; // don't need store because you use "export" on top of your class.
I hope helps anyone out there.
There is also the option to use redux-mock-store.
A mock store for testing Redux async action creators and middleware. The mock store will create an array of dispatched actions which serve as an action log for tests.
The mock store provides the necessary methods on the store object which are required for Redux.
You can specify optional middlewares and your app specific initial state.
import configureStore from 'redux-mock-store'
const middlewares = []
const mockStore = configureStore(middlewares)
const initialState = {}
const store = mockStore(initialState)
const wrapper = mount(<SampleComponent store={store}/>)
You can use name export to solve this problem:
You should have:
class SampleComponent extends React.Component{
...
render(){
<div></div>
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SampleComponent)
You can add a export before class:
export class SampleComponent extends React.Component{
and import this component with no redux store:
import { SampleComponent } from 'your-path/SampleComponent';
With this solution you don't need to import store to your test files.
in an attempt to make the use of decorator syntax more testable I made this:
https://www.npmjs.com/package/babel-plugin-undecorate
input:
#anyOldClassDecorator
export class AnyOldClass {
#anyOldMethodDecorator
method() {
console.log('hello');
}
}
output:
#anyOldClassDecorator
export class AnyOldClass {
#anyOldMethodDecorator
method() {
console.log('hello');
}
}
export class __undecorated__AnyOldClass {
method() {
console.log('hello');
}
}
Hopefully this can provide a solid Option 3!

Redux silently not rendering connect()ed component

I am writing an app using Redux and can't get a Redux connect()ed component to render at all.
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';
var store = createStore((s, a) => s, {hello: "WORLD"});
class App extends React.Component {
render() { return <h1> Hello, world! </h1>; }
}
var connectedApp = connect(function(s) { debugger })(App);
$(document).ready(function() {
var target = document.getElementById("root");
// DOES render
React.render(<App/>, target);
// Never renders
React.render(<connectedApp/>, target);
});
The app is using babel, babelify, redux and redux-react.
Returning an object inside of connect() did not seem to modify this.props in the component, either. The debugger statement passed to connect never fires.
Is there something wrong with this code? Why isn't the component rendering? Why does the debugger statement never fire?
JSX converts component types that start with a capital letter into React.createElement calls to that type:
<App/> // => React.createElement(App);
However, it converts component types that start with lowercase letters into DOM nodes (by passing it as a string instead of a reference):
<connectedApp/> // => React.createElement("connectedApp");
In fact, if you look at the DOM via your browser's inspector, you'll likely see
<connectedApp data-reactid=".0"></connectedApp>
Try capitalizing connectedApp:
var ConnectedApp = connect(...)(App);
// ...
React.render(<ConnectedApp/>, target);
You are not passing a valid ReactElement to your second render method.
The first <App/> component is valid an therefor is being rendered as a DOM node.
The second <connectedApp/> however is not a ReactElement. So it won't be rendered at all. It is just a function. var connectedApp = connect(function(s) { debugger })(App);
Taken from the API from REDUX the typical use of the connect function is as follows:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(App)
with the arguments:
[mapStateToProps(state, [ownProps]): stateProps] (Function)
The App component subscribes to the redux store updates, and this function is always called if the component updates. The return of this function should be a object.
[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function)
As object, every function inside will be recognized as a valid action creator
You don't need to pass the connect to a render method, just subscribe your App to the REDUX store.
So taken from the official REDUX page, this is how you set up the subscription:
import { React } from 'react'
import * as actionCreators from './actionCreators'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actionCreators, dispatch) }
}
class TodoApp extends React.Component {
//your App
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)`

Resources