I implemented load on demand logic in a react/redux application page by invoking a page container mapDispatchToProps method in the page component's componentDidMount method.
The load on demand logic consists of these lines in the page component and its container:
import { isEmpty, isNil } from 'lodash';
import {SimplActions} from 'simpl-react/lib/actions';
class LoadOnDemandPage extends React.Component {
componentDidMount() {
const {run, loadedRun, loadRunData} = this.props;
loadRunData(run, loadedRun);
}
.....
}
LoadOnDemandPage.propTypes = {
run: PropTypes.object.isRequired,
loadedRun: PropTypes.object,
loadRunData: PropTypes.func.isRequired,
};
function mapStateToProps(state, ownProps) {
...
return {
run,
loadedRun: state.simpl.loaded_run,
};
}
const mapDispatchToProps = dispatch => {
return {
loadRunData(run, loadedRun) {
if (!isNil(run)) {
if (isEmpty(loadedRun) || run.pk != loadedRun.pk) {
// load run's world data
const runId = run.id;
const topic = `model:model.run.${runId}`;
dispatch(SimplActions.getDataTree(topic));
dispatch(SimplActions.setLoadedRun(runId));
}
}
},
};
};
const module = connect(
mapStateToProps,
mapDispatchToProps
)(LoadOnDemandPage);
export default withRouter(module);
Now, I'd like to use the same mapDispatchToProps loadRunData method in other pages and other applications by moving it into an external library.
Can this logic be made reusable or must it be replicated in every page component that uses this pattern? FWIW I'm using react v16 and redux v3.
you could create a currying function that takes dispatch, saving in some utils folder:
// above import dependencies
const loadRunDataBuilder = dispatch => (run, loadedRun) {
if (!isNil(run)) {
if (isEmpty(loadedRun) || run.pk != loadedRun.pk) {
// load run's world data
const runId = run.id;
const topic = `model:model.run.${runId}`;
dispatch(SimplActions.getDataTree(topic));
dispatch(SimplActions.setLoadedRun(runId));
}
}
},
now you could import that function at any component and call it passing dispatch
const mapDispatchToProps = dispatch => {
return {
loadRunData: loadRunDataBuilder(dispatch)
},
};
Related
I'm working on a unit test in a React application that verifies a passed in prop function is being conditionally called based on another props value. I'm utilizing Typescript/Enzyme/Jest in my application and am using a Root wrapper around the component I'm testing to inject the Redux store (and override initial state if desired).
import { mount, ReactWrapper } from "enzyme";
import React from "react";
import Login from "src/components/auth/Login";
import Root from "src/Root";
let wrapped: ReactWrapper;
let defaultProps = {
signIn: jest.fn(),
token: null,
};
beforeEach(() => {
wrapped = mount(
<Root>
<Login {...defaultProps} />
</Root>
);
});
describe("on mount", () => {
describe("if no token is supplied to the props", () => {
it("will call the props.signIn() function", () => {
expect(defaultProps.signIn).toHaveBeenCalled();
});
});
});
When I run the test the toHaveBeenCalled() (as well as toBeCalled(), tried both) are not registering any calls. However, I have supplied a console.log statement that is getting triggered within the same conditional that the signIn() function lives.
import React from 'react';
import { AuthState, JWT } from "src/components/auth/types";
import { signIn } from "src/redux/auth";
interface Props {
signIn: () => Promise<void>;
token: null | JWT;
}
class Login extends React.Component<Props> {
/**
* Sign the user in on mount
*/
public componentDidMount(): void {
if (!this.props.token) {
console.log("GETTING HERE");
this.props.signIn();
}
}
public render(): JSX.Elemeent {
// ... More code
}
}
const mapStateToProps = (state: AuthState) => {
return {
token: state.auth.token;
};
};
export default connect(mapStateToProps, { signIn })(Login);
I've gone through several related posts/articles but all of the different configurations, such as traversing enzyme to get the direct prop or utilizing spyOn, have failed.
The only thing I can figure that's different is my wrapping of the Login component with Root, but considering I can see the console.log being triggered this seems like a complete shot in the dark.
Can anyone tell me what I'm doing wrong here?
You've to wait for component to mount, so:
it("will call the props.signIn() function", (done) => {
setImmediate(() => {
expect(defaultProps.signIn).toHaveBeenCalled();
done()
});
});
Ended up being me forgetting to place the override via mapDispatchToProps and mapStateToProps in the connect function. This was causing my passed in signIn function to be overridden by the signIn action imported in the file. Updating with ownProps and conditionally utilizing the passed in value fixes the issue:
const mapStateToProps = (state: AuthState, ownProps: Props) => {
return {
token: ownProps.token || state.auth.token;
};
};
const mapDispatchToProps = (dispatch: ThunkDispatch<{}, {}, any>, ownProps: Props) => {
return {
signIn: ownProps.signIn || (() => { return dispatch(signIn()) })
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Login);
I would like to call 'mapdispatchtoprops' methods into the functional component. But, it is not working as expected"
I have shared some code.
I have created one functional component. here my code
loadHelpers.js
import { connect } from 'react-redux';
import {loadShowInfo,getCurrentSlug }
from '../../actions/requesthandler'
export const loadHelpers= (isManualAccess) =>{
loadShowInfo(isManualAccess)
return null
}
const mapDispatchToProps = dispatch => {
return {
loadShowInfo: (show_info_slug, modeType, season_slug) => dispatch(loadShowInfo(show_info_slug, modeType ,season_slug)),
getCurrentSlug: (currentslug,slugurl) => dispatch(getCurrentSlug(currentslug,slugurl))
}
};
export default connect(null , mapDispatchToProps)(loadHelpers);
loadShowInfo method contains multiple axios calls.
ShowInfo.js
I have called the functional method here.
import {loadHelpers} from '../common/loadHelpers'
loadDatas = (isManualAccess = false , seasonId = "") => {
this.props.loadHelpers(isManualAccess)
}
const mapDispatchToProps = dispatch => {
return {
loadHelpers: () => dispatch(loadHelpers()),
}
};
export default connect(mapStateToProps, mapDispatchToProps)(ShowInfo)
Expected Result:
How to call the functional method into another functional component?
you need to modify loadHelpers. https://reactjs.org/docs/higher-order-components.html
I am following this tutorial to create custom actions in React-admin (former Admin-on-rest): using a custom action creator.
However, after implement it, my code is not doing anything, i.e., the backend is not called.
I guess it is missing in the documentation a way to link the action with the dataProvider, unless the redux is handling it automagically.
Is it right? No need to link to the dataProvider being the opposite from what was made in the fetch example?
Following pieces of my code:
UpdatePage is pretty similar to ApproveButton in the tutorial:
// Update Page.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { updatePage } from '../../actions/pages';
import Button from '#material-ui/core/Button';
class UpdatePage extends Component {
handleClick = () => {
const { record } = this.props;
updatePage(record.id, record);
}
render() {
return <Button disabled={this.props.disabled} onClick={this.handleClick}>Confirmar</Button>;
}
}
UpdatePage.propTypes = {
disabled: PropTypes.bool.isRequired,
updatePage: PropTypes.func.isRequired,
record: PropTypes.object,
};
export default connect(null, {
updatePage
})(UpdatePage);`
Actions for the UpdatePage (like in the commentActions of the tutorial):
//in ../../actions/pages.js
import { UPDATE } from 'react-admin';
export const UPDATE_PAGE = 'UPDATE_PAGE';
export const updatePage = (id, data) => ({
type: UPDATE_PAGE,
payload: { id, data: { ...data, is_updated: true } },
meta: { fetch: UPDATE, resource: 'pages' },
});
You're invoking the action creator directly in the handleClick method. You need to use the updatePage function inside the props to properly dispatch the redux action.
handleClick = () => {
const { record } = this.props;
this.props.updatePage(record.id, record);
}
I have a problem that a react component is rendering before the redux store has any data.
The problem is caused by the React component being rendered to the page before the existing angular app has dispatched the data to the store.
I cannot alter the order of the rendering or anything like that.
My simple React component is
import React, {Component} from 'react';
import { connect } from 'react-redux';
import {addBot} from './actions';
class FlowsContainer extends React.Component {
componentDidMount() {
this.props.initStoreWithBot();
}
render() {
// *** at this point I have the store in state prop
//but editorFlow array is not yet instanced, it's undefined
const tasks = this.props.state.editorFlow[0].flow.tasks
return (
<div>
Flow editor react component in main container
</div>
);
}
}
const mapStateToProps = (state) => ({
state : state
})
const mapDispatchToProps = (dispatch) => {
return {
initStoreWithBot : () => dispatch(addBot("test 123"))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(FlowsContainer)
So how can I hold off the rendering until editorFlow array has elements ?
You can use Conditional Rendering.
import {addBot} from './actions';
class FlowsContainer extends React.Component {
componentDidMount() {
this.props.initStoreWithBot();
}
render() {
// *** at this point I have the store in state prop
//but editorFlow array is not yet instanced, it's undefined
const { editorFlow } = this.props.state;
let tasks;
if (typeof editorFlow === 'object' && editorFlow.length > 0) {
tasks = editorFlow[0].flow.tasks;
}
return (
{tasks &&
<div>
Flow editor react component in main container
</div>
}
);
}
}
const mapStateToProps = (state) => ({
state : state
})
const mapDispatchToProps = (dispatch) => {
return {
initStoreWithBot : () => dispatch(addBot("test 123"))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(FlowsContainer)
As far as I know, you can't.
the way redux works is that it first renders everything, then actions take place with some async stuff(such as loading data), then the store gets populated, and then redux updates the components with the new state(using mapStateToProps).
the lifecycle as I understand it is this :
render the component with the initial state tree that's provided when you create the store.
Do async actions, load data, extend/modify the redux state
Redux updates your components with the new state.
I don't think mapping the entire redux state to a single prop is a good idea, the component should really take what it needs from the global state.
Adding some sane defaults to your component can ensure that a "loading" spinner is displayed until the data is fetched.
In response to Cssko (I've upped your answer) (and thedude) thanks guys a working solution is
import React, {Component} from 'react';
import { connect } from 'react-redux';
import {addBot} from './actions';
class FlowsContainer extends React.Component {
componentDidMount() {
this.props.initStoreWithBot();
}
render() {
const { editorFlow } = this.props.state;
let tasks;
if (typeof editorFlow === 'object' && editorFlow.length > 0) {
tasks = editorFlow[0].flow.tasks;
}
if(tasks){
return (
<div>
Flow editor react component in main container
</div>
)
}
else{
return null;
}
}
}
const mapStateToProps = (state) => ({
state : state
})
const mapDispatchToProps = (dispatch) => {
return {
initStoreWithBot : () => dispatch(addBot("test 123"))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(FlowsContainer)
I have the following component in my react redux project and I'm trying to implement tests but running into problems when trying to test the components
Users.js
export class Users extends Component {
componentWillMount() {
const dispatch = this.context.store.dispatch;
dispatch({ type: UserActions.fetchUsers(dispatch)});
}
render() {
const {users,isFetching} = this.props;
return (
<div>
<CardHeader
title={this.props.route.header}
style={{backgroundColor:'#F6F6F6'}}/>
<UserListHeader/>
{isFetching ? <LinearProgress mode="indeterminate" /> : <UserList users={users}/>}
</div>
)
}
}
Users.propTypes = {
users: PropTypes.array,
actions: PropTypes.object.isRequired
};
Users.contextTypes = {
store: React.PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
users: state.users.users,
isFetching:state.users.isFetching
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(UserActions, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Users);
and I'm trying to test it like the example on the Redux website, but I'm running into issues
UsersSpec.js
import {Users} from '/containers/users/Users'
const middlewares = [ thunk ];
const mockStore = configureMockStore(middlewares);
function usersSetup() {
const props = {
users: expect.createSpy(),
isFetching:expect.createSpy()
};
const enzymeWrapper = mount(<Users />,{context:{store:mockStore}});
return {
props,
enzymeWrapper
}
}
describe('Users', () => {
it('should render self and subcomponents', () => {
const { enzymeWrapper } = usersSetup();
exp(enzymeWrapper.find(UserListHeader)).to.have.length(1);
})
})
But I get the error 'TypeError: dispatch is not a function' should I be mocking the componentWillMount function or how should I test this component.
Should I just be testing the dumb child components? Any guidance would be great. Thanks
mount function provides full dom rendering therefore you'll need to set up jsdom in your test setup. You can see more info here:
Error: It looks like you called `mount()` without a global document being loaded
Another thing is that, you should provide childContextTypes attribute when you're defining context with mount like this:
mount(<Users />, {
context: { store: mockStore },
childContextTypes: { store: PropTypes.object }
});
However if you're writing unit test of a React component you should use shallow function provided by enzyme. It just renders the component with one deep level, so that you don't need to create jsdom and provide context parameters when you're creating the wrapper component. mount rendering in a test means you're actually writing an integration test.
const wrapper = shallow(<Users />);
wrapper.find(UserListMenu).to.have.length(1);