I'm new to react, redux and all the other stuff and I am struggling with some problems. My first problem is that I receive from an axios "get" request as a response my whole side. (I think my webpack makes some trouble) The second problem is that my Axios function will fail with the error "this.props.getBranch is not a function".
I have tried to fix the problems and searched for a while without a solution... So it would be great if you could help me.
my code
Multistep Form
import React, { Component } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import RiskDetails from "./Step1/RiskDetails";
import { getBranch } from "../../actions/riskActions";
class RiskForm extends Component {
constructor(props) {
super(props);
this.state = {
step: 1,
risikoName: "",
risikoBereich: "",
risikoKlasse: "",
branche: ""
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
nextStep() {
let step = this.state.step;
this.setState({ step: step + 1 });
}
prevStep() {
let step = this.state.step;
this.setState({ step: step - 1 });
}
onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
onSubmit(e) {
e.preventDefault();
}
render() {
const { step } = this.state;
const { risikoName, risikoBereich, risikoKlasse, branche } = this.state;
const values = { risikoName, risikoBereich, risikoKlasse, branche };
switch (step) {
case 1:
return (
<RiskDetails
nextStep={this.nextStep}
onChange={this.onChange}
values={values}
getBranch={getBranch}
/>
);
}
}
}
RiskForm.propTypes = {
getBranch: PropTypes.func.isRequired
};
export default connect(
state => {
return {};
},
{ getBranch }
)(RiskForm);
my riskDetails
import React, { Component } from "react";
import { Grid, Form } from "semantic-ui-react";
import PropTypes from "prop-types";
class RiskDetails extends Component {
componentWillMount() {
this.props.getBranch().then(res => {
console.log(res);
});
}
render() {
const { value } = this.props;
return (
<Grid container>
<Grid.Row>
<Grid.Column width={8}>
<Form>
<Form.Select
fluid
label={value.branch}
//options={this.state.branch}
placeholder={value.branch}
/>
</Form>
</Grid.Column>
</Grid.Row>
</Grid>
);
}
}
RiskDetails.propTypes = {
getBranch: PropTypes.func.isRequired
};
export default RiskDetails;
my actions
import axios from "axios";
import { FETCH_BRANCH } from "./types";
export function createRisk(event) {
return dispatch => {
return axios.post("/api/risk", event);
};
}
export function fetchRiskDetails(risk) {
return {
type: FETCH_BRANCH,
risk
};
}
export function getBranch() {
return dispatch => {
console.log("starting get request")
return axios.get("/api/risk").then(res => {
dispatch(fetchRiskDetials(res.data));
console.log(res.data);
});
};
}
reducer
import { FETCH_BRANCH } from "../actions/types";
const initialState = {
risk: {}
};
export default (state = initialState, action = {}) => {
switch (action.type) {
case FETCH_BRANCH:
return {
risk: action.risk
};
default:
return state;
}
};
server index.js
import express from "express";
import path from "path";
import bodyParser from "body-parser";
import mongoose from "mongoose";
import cors from "cors";
//WEBPACK
import webpack from "webpack";
import webpackMiddleware from "webpack-dev-middleware";
import webpackHotMiddleware from "webpack-hot-middleware";
import wepbackConfig from "../webpack.config.dev";
//IMPORT ALL API ROUTES
import authRoute from "./routes/auth";
import signUpRoute from "./routes/signup";
import companyRoute from "./routes/company";
import riskRoute from "./routes/risk";
import recommendationRoute from "./routes/recommendation";
let app = express();
const compiler = webpack(wepbackConfig);
app.use(
webpackMiddleware(compiler, {
hot: true,
publicPath: wepbackConfig.output.publicPath,
noInfo: true
})
);
app.use(cors());
app.use(express.static(path.join(__dirname, "dist")));
app.use(webpackHotMiddleware(compiler));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// SET UP YOUR DB FOR THE API
// =============================================================================
var url = "mongodb://localhost:27017/blubb";
mongoose
.connect(
url,
{ useNewUrlParser: true, useCreateIndex: true }
)
.then(() => console.log("connected to DB"))
.catch(err => console.log(err));
app.get("/*", (req, res) => {
res.sendFile(path.join(__dirname, "./index.html"));
});
// ROUTES FOR OUR API
// =============================================================================
app.use("/api/auth", authRoute);
app.use("/api/risk", riskRoute);
app.listen(3000, () => console.log("running"));
risk part
import express from "express";
import {
validatePostInput,
validateDeleteInput
} from "../shared/validation/risk";
import Risk from "../models/risk";
import authenticate from "../middleware/authenticate";
let router = express.Router();
router.get("/", (req, res) => {
console.log("hi form server");
Risk.find(function(err, risks) {
if (err) {
console.log(err);
} else {
res.status(400).json(risks);
}
});
});
//create new risk
router.post("/", authenticate, (req, res) => {
const { errors, isValid } = validatePostInput(req.body);
console.log(req.body);
if (isValid) {
const { risikoName, risikoBereich, risikoKlasse, createdBy } = req.body;
var newRisk = new Risk({
risikoName,
risikoBereich,
risikoKlasse,
createdBy
});
// save the risk
newRisk.save(function(err) {
if (err) {
return res.status(500).json({ error: err });
}
res.json({ success: true });
});
} else {
res.status(400).json(errors);
}
});
//delete risk
router.delete("/", (req, res) => {
const { errors, isValid } = validateDeleteInput(req.body);
if (isValid) {
const { id } = req.body;
Risk.remove({ _id: id }, function(err, risk) {
if (err) return res.status(500).json({ error: err });
res.json({ success: true });
});
} else {
res.status(400).json(errors);
}
});
export default router;
webpack config
import path from "path";
import webpack from "webpack";
export default {
mode: "development",
entry: [
"webpack-hot-middleware/client",
path.join(__dirname, "client/index.js")
],
output: {
filename: "bundle.js",
path: "/",
publicPath: "/"
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin()
],
module: {
rules: [
{
test: /\.js$/,
include: [
path.join(__dirname, "client"),
path.join(__dirname, "server/shared")
],
loaders: ["react-hot-loader/webpack", "babel-loader"]
},
{
test: /\.css$/,
loader: "style-loader!css-loader"
},
{
test: /\.s[a|c]ss$/,
loader: "sass-loader!style-loader!css-loader"
},
{
test: /\.(jpg|png|gif|jpeg|woff|woff2|eot|ttf|svg)$/,
loader: "url-loader?limit=100000"
}
]
},
resolve: {
extensions: [".js"]
}
};
===EDIT===
Here is my response from a request
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Tool</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
</head>
<body>
<div id="app"></div>
<script src="bundle.js"></script>
</body>
</html>
First, the reason why you always get whole HTML page, I guess it caused by routing management. Looks like the part when you using asterisk/wild card in your route as follows:
app.get("/*", (req, res) => {
res.sendFile(path.join(__dirname, "./index.html"));
});
Looks it will handle all requests, so router describe after that will never get reached. Try to change it with:
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "./index.html"));
});
If you really need it to have /* put another one at very last line of router declaration.
2nd:
You call props inside componentWillMount(), which usually props not yet available at that lifecycle. Even usually it still not available in componentDidMount() lifecycle. Based on some articles I read, componentWillMount() is deprecated in current React version. I suggest you to not using it. There are 2 ways to solve this kind of problem I can recommend you as follow:
componentDidMount() method
Using componentDidMount() lifecycle. In this method, you should put setTimeout() to give the component a time to receive props.
componentDidMount() {
setTimeout(()=>{
this.props.getBranch().then(res => {
console.log(res);
});
},
100);//Try with 0 first ( not put this parameter ), if it is not works, try to increase by 100, 200 etc.
}
2. getDerivedStateFromProps() method
The old way is using componentWillRecieveProps(), but because it is deprecated, I recommend to use getDerivedStateFromProps() instead. In this life cycle phase, it doesn't need setTimeout() trick. Because props should already available.
For your reference, here is good article about deprecated React Livecycles: https://hackernoon.com/problematic-react-lifecycle-methods-are-going-away-in-react-17-4216acc7d58b
==== UPDATE Jan 22 '19 ======
Just a suggestion, usually we can just call redux actions directly to the component instead passing them as props. So you can clean up RiskForm component from Redux wiring as follow:
import React, { Component } from "react";
// import { connect } from "react-redux"; //!!! move connect() inside RiskDetails instead
import PropTypes from "prop-types";
import RiskDetails from "./Step1/RiskDetails";
// import { getBranch } from "../../actions/riskActions"; //!!!!Move this to RiskDetails component
class RiskForm extends Component {
constructor(props) {
super(props);
this.state = {
step: 1,
risikoName: "",
risikoBereich: "",
risikoKlasse: "",
branche: ""
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
nextStep() {
let step = this.state.step;
this.setState({ step: step + 1 });
}
prevStep() {
let step = this.state.step;
this.setState({ step: step - 1 });
}
onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
onSubmit(e) {
e.preventDefault();
}
render() {
const { step } = this.state;
const { risikoName, risikoBereich, risikoKlasse, branche } = this.state;
const values = { risikoName, risikoBereich, risikoKlasse, branche };
switch (step) {
case 1:
return (
<RiskDetails
nextStep={this.nextStep}
onChange={this.onChange}
values={values}
// getBranch={getBranch}//!!! Don't need this props, import inside RiskDetails directly
/>
);
}
}
}
RiskForm.propTypes = {
getBranch: PropTypes.func.isRequired
};
/*
export default connect(
state => {
return {};
},
{ getBranch }
)(RiskForm);*/
export default RiskForm;//You can remove connect() and leave this as dumb component
Then make your RiskDetails component as Container/smart component as follow ( please pay attention at comments I made with exclamation/'!' ):
import React, { Component } from "react";
import {connect} from 'react-redux';//!!! Import redux here
import { Grid, Form } from "semantic-ui-react";
import PropTypes from "prop-types";
import { getBranch } from "../../actions/riskActions";//!!! Import action creator here
class RiskDetails extends Component {
componentWillMount() {
getBranch().then(res => {//Call the action creator directly instead using props
console.log(res);
});
}
render() {
const { value } = this.props;
return (
<Grid container>
<Grid.Row>
<Grid.Column width={8}>
<Form>
<Form.Select
fluid
label={value.branch}
//options={this.state.branch}
placeholder={value.branch}
/>
</Form>
</Grid.Column>
</Grid.Row>
</Grid>
);
}
}
RiskDetails.propTypes = {
getBranch: PropTypes.func.isRequired
};
export default connect(null)(RiskDetails);//!!! Connect this component with redux
Related
I am new to Redux-Saga, have knowledge of Redux.
I am creating a login process using React Native and Redux, Redux-Saga, This is my first redux-saga project and I am not able to fetch the response from call function of the saga.
It just not run the code after yield call(UserService.loginUserApi, params); below is the code.
LoginContainer.js
import React from 'react'
import { Platform, Text, View, Button, ActivityIndicator, Image } from 'react-native'
import { connect } from 'react-redux'
import { ApplicationStyles, Helpers, Images, Metrics } from 'App/Theme'
import LoginActions from 'App/Actions/LoginAction'
class LoginContainer extends React.Component {
componentDidMount() {
}
render() {
return (
<View
style={[
Helpers.fill,
Helpers.rowMain,
Metrics.mediumHorizontalMargin,
Metrics.mediumVerticalMargin,
]}
>
{this.props.isLoading ? (
<View>
<ActivityIndicator size="large" color="#0000ff" />
<Text>{JSON.stringify(this.props.isLoading)}</Text>
</View>
) : (
<View>
<Text >To get started, edit App.js </Text>
{this.props.loginError ? (
<Text >{JSON.stringify(this.props.loginError)}</Text>
) : <Text >{JSON.stringify(this.props.userDetails)}</Text>}
<Button
onPress={() => this.props.loginUser({
"email": "eve.holt#reqres.in",
"password": "cityslicka"
})}
title="Refresh"
/>
</View>
)}
</View>
)
}
}
const mapStateToProps = (state) => {
return ({
userDetails: state.login.userDetails,
isLoading: state.login.isLoading,
loginError: state.login.loginError,
})
};
const mapDispatchToProps = (dispatch) => ({
loginUser: (payload) => dispatch(LoginActions.loginUser(payload)),
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(LoginContainer)
LoginAction.js
const { Types, Creators } = createActions({
// Fetch user informations
loginUser: ['params'],
// The operation has started and is loading
loginLoading: ['value'],
// User informations were successfully fetched
loginUserSuccess: ['user'],
// An error occurred
loginUserError: ['errorMessage'],
})
export const LoginTypes = Types
export default Creators
LoginSaga.js
import { put, call, take } from 'redux-saga/effects'
import LoginActions, { LoginTypes } from 'App/Actions/LoginAction'
import { UserService } from 'App/Services/UserService'
export function* getLogin() {
// while (true) {
// console.log(LoginActions.loginUser());
const action = yield take(LoginTypes.LOGIN_USER);
const { params } = action;
console.log("params", params);
// yield put(LoginActions.loginLoading(true))
let response1 = yield call(UserService.loginUserApi, params);
console.log("it doesnot print anything", response1);
// LoginActions.loginUserError('test');
// console.log("RUN RUn");
// if (response1) {
// console.log("USER", response1);
// yield put(LoginActions.loginUserSuccess(response1))
// } else {
// yield put(
// LoginActions.loginUserError('There was an error while fetching user informations.')
// )
// }
// }
}
Sagas/index.js
import { getLogin } from './LoginSaga'
import { LoginTypes } from 'App/Actions/LoginAction'
export default function* root() {
yield all([
takeLatest(LoginTypes.LOGIN_USER, getLogin),
])
}
LoginReducers.js
import { createReducer } from 'reduxsauce'
import { LoginTypes } from 'App/Actions/LoginAction'
const INITIAL_STATE = {
userDetails: {},
isLoading: false,
loginError: null,
}
export const loginUserError = (state, { errorMessage }) => {
alert();
console.log(errorMessage);
return ({
...state,
userDetails: {},
isLoading: false,
loginError: errorMessage,
})
}
export const loginUserSuccess = (state, { user }) => ({
...state,
userDetails: user,
isLoading: false,
loginError: null,
})
export const loginLoading = (state, { value }) => ({
...state,
userDetails: {},
isLoading: value,
loginError: null,
})
export const loginReducer = createReducer(INITIAL_STATE, {
[LoginTypes.LOGIN_USER_SUCCESS]: loginUserSuccess,
[LoginTypes.LOGIN_USER_ERROR]: loginUserError,
[LoginTypes.LOGIN_LOADING]: loginLoading,
})
Store/index.js
import configureStore from './CreateStore'
import rootSaga from 'App/Sagas'
import { loginReducer } from 'App/Reducers/LoginReducers'
export default () => {
const rootReducer = combineReducers({
login: loginReducer,
})
return configureStore(rootReducer, rootSaga)
}
ConfigureStore.js
import { applyMiddleware, compose, createStore } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { persistReducer, persistStore } from 'redux-persist'
/**
* This import defaults to localStorage for web and AsyncStorage for react-native.
*
* Keep in mind this storage *is not secure*. Do not use it to store sensitive information
* (like API tokens, private and sensitive data, etc.).
*
* If you need to store sensitive information, use redux-persist-sensitive-storage.
* #see https://github.com/CodingZeal/redux-persist-sensitive-storage
*/
import storage from 'redux-persist/lib/storage'
const persistConfig = {
key: 'root',
storage: storage,
/**
* Blacklist state that we do not need/want to persist
*/
blacklist: [
// 'auth',
],
}
export default (rootReducer, rootSaga) => {
const middleware = []
const enhancers = []
// Connect the sagas to the redux store
const sagaMiddleware = createSagaMiddleware()
middleware.push(sagaMiddleware)
enhancers.push(applyMiddleware(...middleware))
// Redux persist
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = createStore(persistedReducer, compose(...enhancers))
const persistor = persistStore(store)
// Kick off the root saga
sagaMiddleware.run(rootSaga)
return { store, persistor }
}
userServices.js
import { Config } from 'App/Config'
import { is, curryN, gte } from 'ramda'
loginUserApi = (userdetails) => {
console.log("IN loginUserApi ", userdetails);
return fetch(Config.API_URL + 'login', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: userdetails.email,
password: userdetails.password,
})
});
// let data = await response.json();
// console.log(data);
// return data;
}
export const UserService = {
loginUserApi,
}
Maybe this call is throwing an error,
let response1 = yield call(UserService.loginUserApi, params);
try to wrap it in a "try/catch" block maybe you have to handle an error case
I'm working in a React project and I'm trying to dispatch an action to make a get request. However I'm receiving 'TypeError: sourceSelector is not a function'.
Here is my code from ViewDocument.js
import React from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import AccountFlowHeader from 'components/Header/AccountFlowHeader';
import { LoadingModal } from 'components/Modals';
import { policiesSelector } from '../Policies/Policies.redux';
import { logOutAction } from '../Login/Login.actions';
import BasePage from '../BasePage';
import { getPolicyStatus } from './PolicyViewDocument.actions';
class PolicyViewDocument extends BasePage {
constructor(props) {
super(props);
this.state = {
showLoaderForPolicyDocument: false,
};
}
componentDidMount() {
super.componentDidMount();
const { currentPolicy: { contractNum } } = this.props;
this.setState({ showLoaderForPolicyDocument: true }, () => {
this.props.getPolicyStatus(contractNum, () => {
this.setState({ showLoaderForPolicyDocument: false });
}, (code) => {
if (code === 'C002') {
console.log(code);
} else {
this.setState({
showLoaderForPolicyDocument: false,
});
}
});
});
}
const mapDispatchToProps = {
getPolicyStatus,
};
export default withRouter(connect(policiesSelector, mapDispatchToProps, { logOutAction })(PolicyViewDocument));
And here is my code from Actions.js
import axios from 'axios';
import urls from 'constants/urls';
import { getErrorCode } from 'utils/common';
import globals from 'constants/globals';
export const getPolicyStatus = (contractNum, scb, fcb) => {
axios({
url: urls.getPolicyStatus(contractNum),
method: 'get',
timeout: globals.timeout,
})
.then((response) => {
if (response.status === 200 && !response.data.errors) {
if (scb) scb(response.data);
} else if (fcb) fcb(getErrorCode(response));
})
.catch((e) => { if (fcb) fcb(getErrorCode(e.response)); });
};
I'm sure it has something to do with how I'm using mapDispatchToProps however I haven't been able to resolve.
Why are you calling super.componentDidMount?? I guess you have used sourceSelector in your parent component, because I am not seeing any sourceSelector thing in your share code snippets.
Try removing super.componentDidMount from here.
I am connecting nodejs backend with reactjs using redux. But every time i submit login form I get an error something like this.
TypeError: Cannot read property 'then' of undefined
and I have no idea where this is coming from. I tried to find this on StackOverflow I did find something that said I need to convert then as a function but still I am getting the same error.
This is my code
loginPage.js
class LoginPage extends Component {
submit = (data) => this.props.login(data).then(() => this.props.history.push('/'));
render() {
return (
<div>
<h1>LoginPage</h1>
<LoginForm submit={this.submit}/>
</div>
);
}
}
loginform.js
onSubmit = (e) => {
e.preventDefault();
const errors = this.validate(this.state.data);
this.setState({ errors });
if (Object.keys(errors).length === 0) {
this.setState({ loading: true});
this.props
.submit(this.state.data)
.catch(err => this.setState({ errors: err.response.data.errors, loading: false }));
}
};
api.js
import axios from 'axios';
export default {
user: {
login: (credentials) =>
axios.post('/api/auth', { credentials })
.then(res => res.data.user),
}
}
actions/auth.js
import { USER_LOGGED_IN } from '../types';
import api from '../api';
export const userLoggedIn = (user) => ({
type: USER_LOGGED_IN,
user
})
export const login = (credentials) => (dispatch) => {
api.user.login(credentials)
.then(user => {
dispatch(userLoggedIn(user))
});
}
backend/routes/auth.js
import express from 'express';
import User from '../Models/User';
const router = express.Router();
router.post('/', (req, res) => {
const { credentails } = req.body;
User.findOne({email: req.body.email}).then(user => {
if (user.email) {
res.json({success: true})
} else {
res.status(400).json({errors: {global: "Invalid Credentials"}})
}
})
})
loginForm.js
import React, {Component} from 'react';
import LoginForm from '../Forms/LoginForm';
import PropTypes from 'prop-types'
import { connect } from 'react-redux';
import { login } from '../../actions/auth';
class LoginPage extends Component {
submit = (data) => this.props.login(data).then(() => this.props.history.push('/'));
render() {
return (
<div>
<h1>LoginPage</h1>
<LoginForm submit={this.submit}/>
</div>
);
}
}
LoginPage.propTypes = {
history: PropTypes.shape({
push: PropTypes.func.isRequired
}).isRequired,
login: PropTypes.func.isRequired
};
export default connect(null, {login})(LoginPage);
You have not specified any Promise to the login function. Thus, you are getting a undefined error.
You can also do this by two ways:
1. Promise
api.user.login(credentials)
.then(user => {
dispatch(userLoggedIn(user))
});
Api.js
import axios from 'axios';
export default {
user: {
login: new Promise(credentials, resolve, reject) =>
axios.post('/api/auth', { credentials })
.then(res => resolve(res.data.user))),
}
}
2. callback function.
actions/auth.js
api.user.login(credentials,
(user => {
dispatch(userLoggedIn(user))
})
);
Api.js
import axios from 'axios';
export default {
user: {
login: (credentials, callback) =>
axios.post('/api/auth', { credentials })
.then(res => callback(res.data.user)),
}
}
I've created a Higher Order Component / Composed Component to ensure a user is authenticated before loading the Component. It's very basic, but I'm having some trouble testing it. I want to test the points below, which are similar to the tests I already have elsewhere:
Renders the Component (I normally check by looking for a Component specific className)
Has correct props (in my case authenticated)
Renders the wrapped Component if authenticated and renders null if not
The HOC:
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { makeSelectAuthenticated } from 'containers/App/selectors';
export default function RequireAuth(ComposedComponent) {
class AuthenticatedComponent extends React.Component {
static contextTypes = {
router: React.PropTypes.object,
}
static propTypes = {
authenticated: React.PropTypes.bool,
}
componentWillMount() {
if (!this.props.authenticated) this.context.router.push('/');
}
componentWillUpdate(nextProps) {
if (!nextProps.authenticated) this.context.router.push('/');
}
render() {
return (
<div className="authenticated">
{ this.props.authenticated ? <ComposedComponent {...this.props} /> : null }
</div>
);
}
}
const mapStateToProps = createStructuredSelector({
authenticated: makeSelectAuthenticated(),
});
return connect(mapStateToProps)(AuthenticatedComponent);
}
I'm using enzyme and jest for my tests, but haven't found a way of rendering the HOC successfully during my tests.
Any ideas?
Solution thanks to answer below:
import React from 'react';
import { shallow, mount } from 'enzyme';
import { Provider } from 'react-redux';
import { AuthenticatedComponent } from '../index';
describe('AuthenticatedComponent', () => {
let MockComponent;
beforeEach(() => {
MockComponent = () => <div />;
MockComponent.displayName = 'MockComponent';
});
it('renders its children when authenticated', () => {
const wrapper = shallow(
<AuthenticatedComponent
composedComponent={MockComponent}
authenticated={true}
/>,
{ context: { router: { push: jest.fn() } } }
);
expect(wrapper.find('MockComponent').length).toEqual(1);
});
it('renders null when not authenticated', () => {
const wrapper = shallow(
<AuthenticatedComponent
composedComponent={MockComponent}
authenticated={false}
/>,
{ context: { router: { push: jest.fn() } } }
);
expect(wrapper.find('MockComponent').length).toEqual(0);
});
});
The "tricky" part here is that your HOC returns a connected component, which makes testing harder because you have shallow render two layers (the connected component and the actual component) and you have to mock the redux store.
Instead you could define the AuthenticatedComponent upfront and export it as a named export. Than you can test it independently of connect like you test every other component:
export class AuthenticatedComponent extends React.Component {
static contextTypes = {
router: React.PropTypes.object,
}
static propTypes = {
authenticated: React.PropTypes.bool,
composedComponent: React.PropTypes.any.isRequired,
}
componentWillMount() {
if (!this.props.authenticated) this.context.router.push('/');
}
componentWillUpdate(nextProps) {
if (!nextProps.authenticated) this.context.router.push('/');
}
render() {
const ComposedComponent = this.props.composedComponent;
return (
<div className="authenticated">
{ this.props.authenticated ? <ComposedComponent {...this.props} /> : null }
</div>
);
}
}
export default function RequireAuth(ComposedComponent) {
const mapStateToProps = () => {
const selectIsAuthenticated = makeSelectAuthenticated();
return (state) => ({
authenticated: selectIsAuthenticated(state),
composedComponent: ComposedComponent,
});
};
return connect(mapStateToProps)(AuthenticatedComponent);
}
Example test:
import React from 'react';
import { shallow, mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import RequireAuth, { AuthenticatedComponent } from '../';
const Component = () => <div />;
Component.displayName = 'CustomComponent';
const mockStore = configureStore([]);
describe.only('HOC', () => {
const RequireAuthComponent = RequireAuth(Component);
const context = { router: { push: jest.fn() } };
const wrapper = mount(
<Provider store={mockStore({})}>
<RequireAuthComponent />
</Provider>,
{
context,
childContextTypes: { router: React.PropTypes.object.isRequired },
}
);
it('should return a component', () => {
expect(wrapper.find('Connect(AuthenticatedComponent)')).toHaveLength(1);
});
it('should pass correct props', () => {
expect(wrapper.find('AuthenticatedComponent').props()).toEqual(
expect.objectContaining({
authenticated: false,
composedComponent: Component,
})
);
});
});
describe('rendering', () => {
describe('is authenticated', () => {
const wrapper = shallow(
<AuthenticatedComponent
composedComponent={Component}
authenticated
/>,
{ context: { router: { push: jest.fn() } } }
);
it('should render the passed component', () => {
expect(wrapper.find('CustomComponent')).toHaveLength(1);
});
});
describe('is not authenticated', () => {
const wrapper = shallow(
<AuthenticatedComponent
composedComponent={Component}
authenticated={false}
/>,
{ context: { router: { push: jest.fn() } } }
);
it('should not render the passed component', () => {
expect(wrapper.find('CustomComponent')).toHaveLength(0);
});
});
});
I'm trying to render the following component on the server side as part of a universal/isomorphic app:
import React, { PropTypes, Component } from 'react';
import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { GridLoader } from 'halogen';
import PostListItem from '../../components/PostListItem/PostListItem';
import { primary as color } from '../../colors';
import { changeSelectedPost, deletePostRequest } from '../../redux/modules/post';
export default connect()(class PostListView extends Component {
static propTypes = {
posts: ImmutablePropTypes.listOf(ImmutablePropTypes.contains({
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
slug: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
})).isRequired,
loading: PropTypes.boolean.isRequired,
dispatch: PropTypes.func.isRequired,
}
handleClick(post) {
this.props.dispatch(changeSelectedPost(post));
}
handleDelete(post) {
if (confirm('Do you want to delete this post')) { // eslint-disable-line
this.props.dispatch(deletePostRequest(post));
}
}
render() {
if (typeof window !== 'undefined' && this.props.loading) {
return (
<div className="container">
<GridLoader color={color} />
</div>
);
}
return (
<div className="listView">
{
this.props.posts.toSeq().map((post, i, arr) => (
<PostListItem
post={post}
key={i}
onClick={this.handleClick.bind(this, post)}
onDelete={this.handleDelete.bind(this, post)}
/>
))
}
</div>
);
}
});
but I receive the error:
module.exports = document.createElement('div').style;
^
ReferenceError: document is not defined
Adding the if block seems to have tripped up the app (the app rendered perfectly before, both server and client side). I'm most likely missing something quite obvious, knowing my track record :P. Any suggestions? :)
UPDATE: File that handles server-side rendering and the rest of the app on the server side:
// server/server.js
'use-strict';
import path from 'path';
import bodyParser from 'body-parser';
import Express from 'express';
import React from 'react';
import webpack from 'webpack';
import { fromJS } from 'immutable';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import { match, RouterContext } from 'react-router';
import { Model } from 'objection';
import { Provider } from 'react-redux';
import { renderToString } from 'react-dom/server';
import config from '../webpack.config.dev';
import routes from '../shared/routes';
import configureStore from '../shared/redux/configureStore';
import assets from './assets';
import db from './db';
import posts from './routes/post.routes';
import Post from './models/post';
import serverConfig from './serverConfig';
// Initialize the Express App
const app = new Express();
Model.knex(db);
if (process.env.NODE_ENV !== 'production') {
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
app.use(webpackHotMiddleware(compiler));
}
// Apply body Parser and server public assets and routes
app.use(bodyParser.json({ limit: '20mb' }));
app.use(bodyParser.urlencoded({ limit: '20mb', extended: false }));
if (process.env.NODE_ENV !== 'production') {
app.use(Express.static(path.resolve(__dirname, '../static')));
}
function getFilename() {
if (process.env.NODE_ENV !== 'production') {
return '"/dist/bundle.js"';
}
return `"/dist/${assets}"`;
}
app.use('/api', posts);
// Render Initial HTML
const renderFullPage = (html, initState, jsFile) => {
return `
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/styles/normalize.css">
<link rel="stylesheet" href="/styles/skeleton.css">
<title>CeresShop</title>
</head>
<div id="root">${html}</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(initState)};
</script>
<script src=${jsFile}></script>
</body>
</html>
`;
};
// Server Side Rendering based on routes matched by React-router.
app.use((req, res) => {
match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {
if (err) {
return res.status(500).end('Internal server error');
}
if (!renderProps) {
return res.status(404).end('Not found!');
}
const initialState = fromJS({
postReducer: {
posts: [],
post: {},
loading: false,
},
route: {
locationBeforeTransitions: null,
},
});
async function loadData() {
if (req.url.includes('post')) {
try {
const newSlug = req.url.substring(5).split('-');
const newId = newSlug[newSlug.length - 1];
const newPost = await Post.query().where('id', newId);
const toBeProcessed = JSON.stringify(newPost[0]);
return initialState.setIn(['postReducer', 'post'], fromJS(JSON.parse(toBeProcessed)));
} catch (error) {
console.log(error);
return initialState;
}
}
try {
const newPosts = await Post.query();
newPosts.sort((a, b) => b.dateadded - a.dateadded);
const toBeProcessed = JSON.stringify(newPosts);
return initialState.setIn(['postReducer', 'posts'], fromJS(JSON.parse(toBeProcessed)));
} catch (error) {
console.log(error);
return initialState;
}
}
loadData().then((currentState) => {
const store = configureStore(currentState);
const createElement = (Component, props) => (
<Component
{...props}
radiumConfig={{ userAgent: req.headers['user-agent'] }}
/>
);
const initialView = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} createElement={createElement} />
</Provider>
);
const finalState = store.getState().toJS();
res.status(200).end(renderFullPage(initialView, finalState, getFilename()));
}).catch(err1 => {
console.log(err1);
res.end(renderFullPage(`Error: ${err1}`, {}, getFilename()));
});
});
});
// start app
app.listen(serverConfig.port, (error) => {
if (!error) {
console.log(`DAT SERVER is running on port: ${serverConfig.port}!`); // eslint-disable-line
}
});
export default app;
If you have a component that is being rendered server-side that requires another component which will only will be rendered in the client, it will still attempt to export that module on the server. You can either check if document exists, or require the module inline.
module.exports = typeof document !== 'undefined' ? document.createElement('div').style : null
// or
componentDidMount() {
// this is only called in the client
require('./clientComponent')
}