I written a custom logic for handling async route loading bundles in react-router-dom .v4. It's work perfectly. But also I heard about useful package with nice API to do the same, like React-Loadable. It has one problem, I cannot get the props/state pushed from Redux on the mount of the component throw this package.
My code is rewritten from the custom style to react-loadable style in two examples below. The last one is react-loadable version, that does not throw state/props.
My personal code:
const asyncComponent = getComponent => {
return class AsyncComponent extends React.Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentWillMount() {
const { Component } = this.state
if (!Component) {
getComponent().then(({ default: Component }) => {
const { store } = this.props // CAN GET THE REDUX STORE
AsyncComponent.Component = Component;
this.setState({ Component });
});
}
}
render() {
const { Component } = this.state;
if (Component) {
return <Component {...this.props} />
}
return null;
}
};
};
export default withRouter(asyncComponent(() => import(/* webpackChunkName: "chunk_1" */ './containers/Component')))
The same code, but with React-Loadable:
const Loading = () => {
return <div>Loading...</div>;
}
const asyncComponent = Loadable({
loader: () => import(/* webpackChunkName: "" */ './containers/Component')
.then(state => {
const { store } = this.props // CANNOT GET THE REDUX STORE!!
}),
loading: Loading
})
export default withRouter(asyncComponent)
To get the state from Redux store via Provider you should place your asyncComponent in Stateful Component wrapper, like you do in your custom async logic (1st case).
It because Loadable library returns you asyncComponent like a function, not a constructor, that way he cannot get access to current Redux store. So, the working solution will be the next:
const Loading = () => {
return <div>Loading...</div>;
}
const asyncComponent = Loadable({
loader: () => import(/* webpackChunkName: "" */ './containers/Component')
.then(state => {
const { store } = this.props // YOU WILL GET THE REDUX STORE!!
}),
loading: Loading
})
class asyncComponentWrapper extends Component{ // Component wrapper for asyncComponent
render() {
return <asyncComponent {...this.props} />
}
}
export default withRouter(asyncComponentWrapper)
P.S.
I do not know what you try to do, but in case how to make reducer injection inside the current store (probably it's exactly what you trying to do), you need to include you Redux store explicitly by import, not from the Provider state.
Related
Is there another way of using ES6 destructuring in a React Class component without having to do it in each method?
I am using the same prop (this.prop.page) in the constructor, componentDidMount(), componentDidUpdate() and render() methods:
class SinglePage extends React.Component {
constructor(props) {
super(props);
const { page } = this.props;
//...
}
componentDidMount() {
const { page } = this.props;
//...
}
componentDidUpdate() {
const { page } = this.props;
//...
}
render() {
const { page } = this.props;
return (
//...
);
}
}
exports default SinglePage;
Is there a way of do it just once?
There is if you can use latest react version with hooks. UseEffect will replace didMount and didUpdate and also no constructor with functional component. I recommend to read this article about useEffect hook. https://overreacted.io/a-complete-guide-to-useeffect/
useEffect is there to handle the cases you would use lifecycle methods for in class components. You can use one or more, depending on your needs.
import React, { useEffect } from React;
function SinglePage({ page }) {
useEffect(() => {
// componentDidMount() {
}, []); // empty array here means it'll only run after the first render
useEffect(() => {
// componentDidMount() {
// componentDidUpdate() {
}); // no second are means it runs after every render
useEffect(() => {
// componentDidMount() {
// componentDidUpdate() {
}, [page]); // runs on initial render and whenever `page` changes
useEffect(() => {
return () => cancelTheThings(); // componentWillUnMount() {
}); // return a function from your useEffect function to have it run before unmount
return {
//...
}
}
export default SinglePage;
Having very simple component:
import PropTypes from 'prop-types'
import React from 'react'
import { connect } from 'react-redux'
class MyComponent extends React.Component {
componentWillMount() {
if (this.props.shouldDoSth) {
this.props.doSth()
}
}
render () {
return null
}
}
MyComponent.propTypes = {
doSth: PropTypes.func.isRequired,
shouldDoSth: PropTypes.bool.isRequired
}
const mapStateToProps = (state) => {
return {
shouldDoSth: state.shouldDoSth,
}
}
const mapDispatchToProps = (dispatch) => ({
doSth: () => console.log('you should not see me')
})
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
I want to test if doSth is called when shouldDoSth is equal true.
I've written a test:
describe('call doSth when shouldDoSth', () => {
it('calls doSth', () => {
const doSthMock = jest.fn()
const store = mockStore({shouldDoSth: true})
shallow(<MyComponent doSth={doSthMock}/>, { context: { store } }).dive()
expect(doSthMock).toHaveBeenCalled()
})
})
but it seems that although I pass doSth as props it gets overridden by mapDispatchToProps as console.log('im not a mock') is executed.
How to properly pass/override/assign doSth function to make component use mock instead of function from mapDispatchToProps. Or maybe I'm doing something which should not be allowed at all and there is 'proper' way of testing my case. Shall I just mock dispatch instead and check if it is called with proper arguments?
I think one thing you need to figure out is whether you want doSth to be a prop, or a redux action connected in mapDispatchToProps.
If it's a prop, then you would connect it to redux in a parent (container). Remove it from this component's mapDispatchToProps. This would make the component more testable.
If you want it to be a redux action connected in this component, then it would make sense to move the action out of this component, somewhere like actions.js, import it in this component, and then mock it in the test jest.mock('actions.js', () => ({doSth: jest.mock()}))
Export the unconnected component and use it in the test and you will be able to override the mapDispatchToProps action.
export class MyComponent extends React.Component {
componentWillMount() {
if (this.props.shouldDoSth) {
this.props.doSth()
}
}
render () {
return null
}
}
MyComponent.propTypes = {
doSth: PropTypes.func.isRequired,
shouldDoSth: PropTypes.bool.isRequired
}
const mapStateToProps = (state) => {
return {
shouldDoSth: state.shouldDoSth,
}
}
const mapDispatchToProps = (dispatch) => ({
doSth: () => console.log('you should not see me')
})
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
import {MyComponent} from '../MyComponent'
describe('call doSth when shouldDoSth', () => {
it('calls doSth', () => {
const doSthMock = jest.fn()
const store = mockStore({shouldDoSth: true})
shallow(<MyComponent doSth={doSthMock}/>, { context: { store } }).dive()
expect(doSthMock).toHaveBeenCalled()
})
})
I think that you should ask yourself if you want to test the unconnected MyComponent or the connected one.
Here you have two discussions that may help you: Am I testing connected components correclty? and Can't reference containers wrapped in a Provider or by connect with Enzyme
If you are not testing the action nor state properly said, you might forget about mapStateToProps and mapDispatchToProps (those processes are already tested by redux) and pass values through props.
Check the following example:
describe('MyComponent', () => {
let wrapper;
const doSthMock = jest.fn();
beforeEach(() => {
const componentProps = {
doSth: true,
};
wrapper = mount(
<MyComponent
{... componentProps}
doSth={doSthMock}
/>
);
});
it('+++ render the component', () => {
expect(wrapper.length).toEqual(1);
});
it('+++ call doSth when shouldDoSth', () => {
expect(doSthMock).toHaveBeenCalled();
});
})
I have such component. It is a wrapper component for another component. There is onClick function, which should call the log if is mouse event
import log from './log';
export function withPressedLog(
Component,
options,
) {
class WithPressedLogComponent extends React.Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
public render() {
const { ...props } = this.props;
return <Component {...props} onClick={this.onClick} />;
}
private onClick(e) {
if (this.props.onClick !== undefined) {
this.props.onClick(e);
}
if (e) {
this.props.log();
}
}
}
const mapDispatchToProps = {
log: () => log(options),
};
return connect(
undefined,
mapDispatchToProps,
)(WithPressedLogComponent);
}
I need to test is it called this.props.log. I have a unit test, but it not works. How I can do it using jest, enzyme?
it("should not log if has not mouse event", () => {
const onClickMock = jest.fn();
const logMock = jest.fn();
const ButtonWithLog = withPressedLog(Button, {
type: "BUTTON_PRESSED",
});
const subject = mountProvider(ButtonWithLog, { onClick: onClickMock, log: logMock });
const mockedEvent = { target:{} };
subject.find(ButtonWithLog).simulate("click", mockedEvent);
expect(onClickMock.mock.calls).toHaveLength(1);
expect(logMock.mock.calls).toHaveLength(0); // not works correctly, always return []
});
store
const store = createStore(() => ({}));
const dispatchMock = jest.fn();
store.dispatch = dispatchMock;
mountProvider function
function mountProvider(
Component,
props,
) {
return mount(
<Provider store={store}>
<Component {...props} />
</Provider>,
);
}
I think the problem here is that you are actually testing the connected component, not the unwrapped component.
Try to isolate the components you are testing more. For example, you can use enzyme's shallow wrapper and the dive method on the connected component, to directly get to the unwrapped component.
Specifically, your problem could be that your connected component is getting the log prop from the store (through mapDispatchToProps), but the store is mocked, so it does not work. In your test, you pass some mock function as prop to the component, but the reference gets lost once the components connects.
helpful thread on github
I have a React decorator component which is connected to the Redux store and I'm using it to dispatch an action (which is used to get some data from an API endpoint) and show a Loader Component. Then, once the data is fetched, it shows a wrapped component.
It looks like this:
const loadData = LoaderComponent => WrappedComponent => {
class loadDataHOC extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
const {fetchData, isLoading} = this.props;
if(!isLoading){
fetchData();
}
}
render() {
const {data, isLoading} = this.props;
if (isLoading) {
return <LoaderComponent />;
}
return <WrappedComponent data={data} />;
}
}
const mapStateToProps = state => {
return {
data: getData(state),
isLoading: getIsLoading(state)
};
};
const mapDispatchToProps = dispatch => bindActionCreators({getData}, dispatch);
return connect(mapStateToProps, mapDispatchToProps)(loadDataHOC);
};
export default loadData;
This component is meant to be reusable so I can use it to fetch and store the same data from different presentational components. What I'd like to do now is to use this component in two different parts of the same view, like this:
const EnhancedComponent1 = loadData(Spinner)(MyPresentationalComponent1);
const EnhancedComponent2 = loadData(Spinner)(MyPresentationalComponent2)
The problem is that the two EnhancedComponent both fire fetchData() because they are mounted together and therefore the isLoading prop is false in both the function calls.
For now I've solved it by checking the isLoading prop inside the action so the second call is immediately stopped, but I'm not sure if this is the best way to deal with it.
const getData = () => (dispatch, getState) => {
if(getIsLoading(getState())) {
return;
}
dispatch(getData());
...
};
Another way to do it would be to create only one parent enhanced component just to fetch the data and then two presentational components down the tree that only access the state, but I'd like to fetch the data as close as possible to the presentational component.
Thanks
Have problem with state in my component.
I'm trying to get status from my reducer but state is empty just getting undefined
Here is my actionCreator
export function checkLogin() {
return function(dispatch){
return sessionApi.authCheck().then(response => {
dispatch(authSuccess(true));
}).catch(error => {
throw(error)
})
}
}
My reducer
export const authStatus = (state = {}, action) => {
switch(action.type){
case AUTH_FALSE:
return{
status: action.status
}
case AUTH_TRUE:
return {
...state,
status: action.status
};
default:
return state;
}
};
And here is my component where i'm trying to get state
const mapStateToProps = (state) => {
return {
status: state.status
}
};
const mapDispatchToProps = (dispatch:any) => {
const changeLanguage = (lang:string) => dispatch(setLocale(lang));
const checkAuth = () => dispatch(checkLogin());
return { changeLanguage, checkAuth }
};
#connect(mapStateToProps, mapDispatchToProps)
I need to get status from the state
Component
import * as React from "react";
import Navigation from './components/navigation';
import { connect } from 'react-redux';
import { setLocale } from 'react-redux-i18n';
import cookie from 'react-cookie';
import {checkLogin} from "./redux/actions/sessionActions";
class App extends React.Component<any, any> {
constructor(props:any) {
super(props);
this.state = {
path: this.props.location.pathname
};
}
componentDidMount(){
this.props.checkAuth();
this.props.changeLanguage(cookie.load('lang'));
}
componentWillUpdate(){
}
render() {
return (
<div>
<Navigation path={this.state.path} />
{this.props.children}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
status: state.status
}
};
const mapDispatchToProps = (dispatch:any) => {
const changeLanguage = (lang:string) => dispatch(setLocale(lang));
const checkAuth = () => dispatch(checkLogin());
return { changeLanguage, checkAuth }
};
#connect(mapStateToProps, mapDispatchToProps)
export class Myapp
extends App {}
You cannot access props that are asynchronous inside of the constructor. As the constructor will be executed only once, when you instantiate your component. When you instantiate your component your asynchronous call has not responded yet, therefore this.props.status is undefined.
You could use componentWillReceiveProps from React lifecycle methods for example:
componentWillReceiveProps(nextProps) {
console.log(nextProps.status);
}
This method will be executed everytime a prop connected, or passed, to the component will change.
You could also use this.props.status inside of the render as this one is also executed everytime a prop changed.
For a better understanding of react lifecycle you could have the look at the different methods available, here : https://facebook.github.io/react/docs/react-component.html