I'm new to redux and having trouble wrapping my head around presentational and container components.
Relevant stack:
react v0.14.8
react-native v0.24.1
redux v3.5.2
react-redux v4.4.5
The issue:
I have a login button component, which when rendered checks the login status and calls the onSuccessfulLogin action which updates the state with the user's Facebook credentials.
However, when trying to separate this into separate presentational/container components, I'm unable to call the onSuccessfulLogin action: Error: onSuccessfulLogin is not defined.
What am I doing wrong here? I'd imagine there's something simple that I'm not understanding with the relationship between the two components and the connect() function.
Presentational Component (Login.js)
import React, { PropTypes } from "react-native";
import FBLogin from "react-native-facebook-login";
import UserActions from "../users/UserActions";
class LoginPage extends React.Component {
render() {
const { userData, onSuccessfulLogin } = this.props;
return (
<FBLogin
permissions={["email","user_friends"]}
onLoginFound= { data => {
onSuccessfulLogin(data.credentials);
}}
/>
)
}
};
export default LoginPage;
Container Component (LoginContainer.js)
import { connect } from 'react-redux';
import LoginPage from "../login/LoginPage";
import UserActions from "../users/UserActions";
const mapDispatchToProps = (dispatch) => {
return {
onSuccessfulLogin: (userData) => {
dispatch(UserActions.userLoggedIn(userData))
}
}
}
const mapStateToProps = (state) => {
return {
userData: state.userData
}
}
const LoginContainer = connect(
mapStateToProps,
mapDispatchToProps
)(LoginPage);
export default LoginContainer;
Also, if I wanted to make the updated state.userData accessible to the LoginPage component, how would I do that? Any help is appreciated!
Solved! When using ES6 classes, you're required to call super(props) in a constructor method in order to access the container's properties in the connected presentational component:
class LoginPage extends React.Component {
constructor(props){
super(props);
}
render(){
// ...
}
}
Your container component is supposed to be a component and it must have a render function with the dumb/presentational components you want to render.
import { connect } from 'react-redux';
import LoginPage from "../login/LoginPage";
import UserActions from "../users/UserActions";
class LoginContainer extends React.Component {
constructor(props){
super(props);
}
render() {
return (
<LoginPage userData={this.props.userData}
onSuccessfulLogin={this.props.onSuccessfulLogin}
/>
)
}
};
const mapDispatchToProps = (dispatch) => {
return {
onSuccessfulLogin: (userData) => {
dispatch(UserActions.userLoggedIn(userData))
}
}
}
const mapStateToProps = (state) => {
return {
userData: state.userData
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(LoginPage);
Related
I am building an app with react / redux for managing Collection of Electronic equipment (=donations). I have several routes that their functionality - is similiar - fetching entity (it could be volunteer, donor etc) data and show it in a table.
the volunteer route:
import React, {Component} from 'react';
import { connect } from 'react-redux';
import { requestVolunteerData } from '../actions/entitiesAction';
import { volenteerColumns as columns } from '../utils/entitiesColumns/volenteerColumns';
import '../container/App.css';
import Table from '../components/Table/Table';
import Loading from '../components/Loading/Loading';
const mapStateToProps = state => {
return {
entities: state.requestEntitiesReducer.entities,
isPending: state.requestEntitiesReducer.isPending,
error: state.requestEntitiesReducer.error
}
}
const mapDispatchToProps = dispatch => {
return {
onRequestEntities: () => dispatch(requestVolunteerData())
}
}
class Volenteer extends Component{
componentDidMount () {
this.props.onRequestEntities();
}
render () {
const { entities, isPending} = this.props;
return isPending ?
<Loading />
:
(
<div className='tc'>
<h1 className='f2'>רשימת מתנדבים</h1>
<Table data={ entities } columns={ columns } />
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Volenteer);
and a consumer route look like this:
import React, {Component} from 'react';
import { connect } from 'react-redux';
import { requestConsumerData } from '../actions/entitiesAction';
import { consumerColumns as columns } from '../utils/entitiesColumns/consumerColumns';
import '../container/App.css';
import Table from '../components/Table/Table';
import Loading from '../components/Loading/Loading';
const mapStateToProps = state => {
return {
entities: state.requestEntitiesReducer.entities,
isPending: state.requestEntitiesReducer.isPending,
error: state.requestEntitiesReducer.error
}
}
const mapDispatchToProps = dispatch => {
return {
onRequestEntities: () => dispatch(requestConsumerData())
}
}
class Consumer extends Component{
componentDidMount () {
this.props.onRequestEntities();
}
render () {
const { entities, isPending} = this.props;
return isPending ?
<Loading />
:
(
<div className='tc'>
<h1 className='f2'>רשימת נזקקים</h1>
<Table data={ entities } columns={ columns }/>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Consumer);
As you can see, they both have the same logic and the differences are:
the action
the Entity name for the h1 tag
the columns object
the data of course
so I tried to implement an HOC which look like this:
import React, {Component} from 'react';
import { connect } from 'react-redux';
import '../container/App.css';
import Table from '../Table/Table';
import Loading from '../Loading/Loading';
export default function WithEntity (EntityComponent, action, columns, name) {
const mapStateToProps = state => {
return {
isPending: state.requestEntitiesReducer.isPending,
entities: state.requestEntitiesReducer.entities,
error: state.requestEntitiesReducer.error
}
}
const mapDispatchToProps = dispatch => {
return {
onRequestEntities: () => dispatch(action)
}
}
class extends Component {
componentDidMount () {
this.props.onRequestEntities();
}
render() {
return (
<EntityComponent {...this.props} />
)
}
}
return connect(mapStateToProps, mapDispatchToProps)(EntityComponent);
}
and the volunteer should look like:
const volunteerHoc = WithEntity (volunteer, action, columns, name);
const consumerHoc = WithEntity (consumer, action, columns, name)
but I did not understand how to inject the Loading and Table components, and wht the name of the class inside the HOC should be-
should I use another HOC - something like WithLoader that receive the data from the first one and render the Loading and Table components with the proper data? just to mention that connect is HOC itself so I need to return the EntityComponent to the redux store :
return connect(mapStateToProps, mapDispatchToProps)(EntityComponent);
I Would appreciate any help
OK, I made it, the HOC takes a basic component, Expands the functionality (by adding methods and managing state for ex) and return a new (henanced) comp with this props.
lets create a simple volunteer comp:
import React, {Component} from 'react';
import { requestVolunteerData } from '../actions/entitiesAction';
import { volenteerColumns as columns } from '../utils/entitiesColumns/volenteerColumns';
import '../container/App.css';
import WithEntity from '../components/HOC/WithEntity.jsx';
import Table from '../components/Table/Table';
import Loading from '../components/Loading/Loading';
class Volenteer extends Component {
render() {
const { entities, isPending} = this.props;
return isPending ?
<Loading />
:
(
<div className='tc'>
<h1 className='f2'>רשימת מתנדבים</h1>
<Table data={ entities } columns={ columns } />
</div>
);
}
}
const VolenteerHOC = WithEntity(Volenteer, requestVolunteerData() );
export default VolenteerHOC;
now lets create the HOC WithEntity that managing the state and return the new cmop to redux state by connect:
import React, {Component} from 'react';
import { connect } from 'react-redux';
const WithEntity = (EntityComponent, action) => {
const mapStateToProps = state => {
return {
isPending: state.requestEntitiesReducer.isPending,
entities: state.requestEntitiesReducer.entities,
error: state.requestEntitiesReducer.error
}
}
const mapDispatchToProps = dispatch => {
return {
onRequestEntities: () => dispatch(action)
}
}
class NewCmoponent extends Component {
componentDidMount () {
this.props.onRequestEntities();
}
render() {
const { entities, isPending} = this.props;
return (
<EntityComponent {...this.props} />
)
}
}
return connect(mapStateToProps, mapDispatchToProps)(NewCmoponent );
}
export default WithEntity;
Now same route can be simply generated via this HOC.
check out this video:
https://www.youtube.com/watch?v=rsBQj6X7UK8
I'm using react with react-native and redux. The error comes to the component from the redux store. After that, i received: Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
What is wrong with this? why hoc can't see the props?
My component:
import React, { Component } from 'react';
import withHandleError from './withHandleError';
class SendScreen extends Component {
render() {
const { error } = this.props;
return (
<div> Test </div>
)
}
};
const mapStateToProps = ({ppm}) => ({
error: ppm.error
})
export default withHandleError(connect(mapStateToProps)(SendScreen));
And HoC:
import React, { Component } from 'react';
import { ErrorScreen } from '../../ErrorScreen';
import { View } from 'react-native';
export default Cmp => {
return class extends Component {
render() {
const {error, ...rest } = this.props;
console.log(error) //// undefined....
if (error) {
return <ErrorScreen />
}
return <Cmp { ...rest } />
}
}
}
The order is which you call the HOCs matters when you want to access props supplied by one in another. Re-ordering your connect and withHandleError HOC will work
import React, { Component } from 'react';
import withHandleError from './withHandleError';
class SendScreen extends Component {
render() {
const { error } = this.props;
return (
<div> Test </div>
)
}
};
const mapStateToProps = ({ppm}) => ({
error: ppm.error
})
export default connect(mapStateToProps)(withHandleError(SendScreen));
I've created a React Native app with navigation provided by react-navigation, also integrating redux with react-navigation-redux-helpers, and I'm trying to figure out a good way of implementing a globally-available 'SignOutHeaderButton' component that, when pressed, will dispatch a redux action and perform a navigation operation.
At the moment, I'm having to pass a function via screenProps from the application root component, which is the function that dispatches the redux action. This function is then passed to the UpdatesListView container component via screenProps, and is then passed into the SignOutHeaderButton common component as a prop via navigationOptions.
Is there a better way in which I can implement this so that I don't have to pass any props into the SignOutHeaderButton component and without having to instantiate a signOut() function within each container component in the application from which there will be a 'Sign Out' button?
App.js:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { reduxifyNavigator } from 'react-navigation-redux-helpers';
import { PersistGate } from 'redux-persist/integration/react';
import { Provider, connect } from 'react-redux';
import { appContainer } from './src/navigators';
import { store, persistor } from './src/store/configureStore';
import { signOut } from './src/actions/auth';
const app = reduxifyNavigator(appContainer, 'root');
const mapStateToProps = state => {
return {
state: state.navReducer
}
}
const AppWithNavigationState = connect(mapStateToProps)(app);
export default class App extends React.Component {
signOut() {
store.dispatch(signOut());
}
render() {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<AppWithNavigationState
screenProps={{
signOut: this.signOut
}} />
</PersistGate>
</Provider>
)
}
}
UpdatesListView.js:
import React from 'react';
import { View, Container, Content, Text, Button } from 'native-base';
import { connect } from 'react-redux';
import SignOutHeaderButton from '../components/common/SignOutHeaderButton';
class UpdatesListView extends React.Component {
constructor(props) {
super(props);
}
static navigationOptions = ({ navigation }) => {
return {
headerTitle: 'Updates',
headerRight: <SignOutHeaderButton signOut={navigation.getParam('signOut')} />
}
}
componentDidMount() {
this.props.navigation.setParams({
signOut: this.props.screenProps.signOut
})
}
render() {
return (
<Container>
<Text>UpdatesListView</Text>
</Container>
)
}
}
const mapStatetoProps = state => {
return {
updates: state.updatesReducer,
tags: state.tagsReducer
}
}
export default connect(mapStateToProps)(UpdatesListView);
SignOutHeaderButton.js:
import React from 'react';
import { Button } from 'react-native';
import { withNavigation } from 'react-navigation';
class SignOutHeaderButton extends React.Component {
constructor(props) {
super(props);
}
signOut() {
this.props.signOut();
this.props.navigation.navigate('AuthStack');
}
render() {
return (
<Button
title="Sign Out"
onPress={this.signOut} />
)
}
}
export default withNavigation(SignOutHeaderButton);
Use redux connect, the idea of redux connect is to bind the store dispatch state to your component this, When you use redux connect you can access any redux store dispatch and state from anywhere in your application this works for both react and react-native, for example for SignOutHeaderButton.js:
import React from 'react';
import { Button } from 'react-native';
import { connect } from 'react-redux';
import { withNavigation } from 'react-navigation';
class SignOutHeaderButton extends React.Component {
constructor(props) {
super(props);
}
signOut() {
this.dispatch(signOut());
this.props.navigation.navigate('AuthStack');
}
render() {
return (
<Button
title="Sign Out"
onPress={this.signOut} />
)
}
}
export default connect()(withNavigation(SignOutHeaderButton));
Also, you can pass the redux store state to your component by passing a function to your connect(/*your function*/) to resolve the state data you want.
for better understanding, you can try this tutorial: https://blog.logrocket.com/react-redux-connect-when-and-how-to-use-it-f2a1edab2013
Note using nested smart components is very common, redux connect has a very smart algorithm to compare the store state changes, please check this: https://hackernoon.com/why-using-nested-connect-react-redux-components-is-good-bd17997b53d2
For example: i have 2 controll-view container user.cv.jsx and sidebar.cv.jsx
Screen consist of User and Sidebar. Sidebar rendering in User screen.
User container:
import React from 'react'
import {Link} from 'react-router-dom';
import { connect } from 'react-redux';
import UserTypeComponents from '../components/user_type.jsx'
import Sidebar from '../../sidebar/containers/sidebar.cv.js'
import * as showList from '../action/list.action.js';
import * as userLimit from '../action/limit.action.js';
import PropTypes from 'prop-types'
function mapStateToProps (state) {
return {...state}
}
class UserType extends React.Component {
constructor (props, context) {
super(props);
this.context = context;
if(!this.props.oauth.isAuthenticating) {
this.context.router.history.push('/login');
return;
}
}
componentDidMount() {
}
render() {
console.log(this.props);
return (<div>
<Sidebar />
<UserTypeComponents {...this.props} />
</div>);
}
}
UserType.contextTypes = {
router: PropTypes.object
}
export default connect(mapStateToProps)(UserType);
And Sidebar Container:
import React from 'react'
import {Link} from 'react-router-dom';
import ShowSidebar from '../components/sidebar.jsx';
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import Preloader from '../../../helpers/preloader.helper.js'
import * as active from '../action/active.action.js'
import * as list from '../action/list.action.js'
import * as show from '../action/show.action.js'
import {DEFAULT_COMPONENTS} from '../constant/sidebar.const.js';
function mapStateToProps (state) {
return state.sidebar
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
...active,
...list,
...show
}, dispatch);
}
class Sidebar extends React.Component {
constructor (props) {
super(props);
}
listOfLinks(){
const makeRequest = async () => {
try {
const data = await (await fetch('http://localhost:3000/sidebar')).json(),
active = this.activeComponent(data);
this.props.list(data);
this.props.active(active);
} catch (err) {
console.log(err)
}
}
makeRequest()
}
activeComponent(data){
for(let key of data){
if(location.pathname.indexOf(key.name.toLowerCase()) != -1){
return key.name.toLowerCase();
}
}
return DEFAULT_COMPONENTS;
}
componentWillMount() {
this.listOfLinks();
}
activeSidebarState(event){
let parent = event.target.parentNode,
target = _$('.site-sidebar__name', parent),
text = target.innerText.toLowerCase();
this.props.active(text);
}
render() {
const loading = this.props.sidebar.links.length;
return (loading ? <ShowSidebar changeActive={::this.activeSidebarState} active={this.props.sidebar.active} links={this.props.sidebar.links} /> : <Preloader />);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Sidebar);
For all this, action and redusers are written. The sidebar sends a request to the server and requests all the modules and forms links to them, too. The user module is accessing the server and requires all users. The problem is that the preloader is being formed in the sidebar, and when the sidebar is loaded the preloader disappears. But the users still could not boot.
The question is: How to control the loading of the sidebar and the user, so that when these two components are updated, the state remove the preloader.
A common practice is to store isFetching flag in the reducer and update it in respond to fetch actions. For example:
function users(state = { users: [], isFetching: false }, action) {
switch (action.type) {
case 'FETCH_USERS_START':
return { ...state, isFetching: true };
case 'FETCH_USER_SUCCESS':
return { ...state, isFetching: false, users: action.payload.users };
default:
return state;
}
}
Then you can access it from both your components via mapStateToProps and show the preloader.
A main thing here is that you need to move the async call to an action, so reducer will be able to react to it. You can use redux-thunk middleware.
I have a (React) container component. It's children need different data from different api endpoints, so I want to dispatch 2 actions the same time (both are asynchronous).
This doesn't seem to be possible. If I have both dispatches, the activeSensors are always empty...
class Dashboard extends React.Component {
static propTypes = {
userData: React.PropTypes.array.isRequired,
activeSensors: React.PropTypes.object.isRequired
};
static contextTypes = {
store: React.PropTypes.object
};
constructor(props) {
super(props);
}
componentWillMount() {
const { store } = this.context;
store.dispatch(fetchActiveSensorDataForAllSensors());
store.dispatch(fetchUserData());
}
render() {
return (
<div>
<AnalyticsPanel activeSensors={this.props.activeSensors}/>
<SearchCustomer userData={this.props.userData}/>
</div>
);
}
}
export default connect((state)=> {
return {
userData: state.userData.data,
activeSensors: state.activeSensorsAll.sensors
}
})(Dashboard);
EDIT: See the source for the full component.
I haven't used the this.context.store.dispatch method your code uses, but I don't think that its necessarily the way you should be doing things. Primarily because it really muddies the line between container and presentational components. Presentational components don't need access to store, and there are other methods to do this which don't have this (albeit pedantic) drawback.
My component files typically look like this:
import React from 'react';
import { connect } from 'react-redux';
import * as actions from './actions';
export class Container from React.Component {
componentWillMount() {
// Most conical way
const { fetchActiveSensorDataForAllSensors, fetchUserData } = this.props;
fetchActiveSensorDataForAllSensors();
fetchUserData();
// Less conical way
// const { dispatch } = this.props;
// const { fetchActiveSensorDataForAllSensors, fetchUserData } = actions;
// dispatch(fetchActiveSensorDataForAllSensors());
// dispatch(fetchUserData());
}
render() {
return (
<div>
<AnalyticsPanel activeSensors={this.props.activeSensors}/>
<SearchCustomer userData={this.props.userData}/>
</div>
);
}
}
function mapStateToProps(state) {
return {
activeSensors: state.activeSensorsAll.sensors,
userData: state.userData.data
}
}
export default connect(mapStateToProps, actions)(Container);