Testing react-router v4 routing with redux connected components - javascript

I am trying to verify that my routes are properly set up. As in when I have a certain URI, then the appropriate component gets loaded. Just to prevent me from mistyping a path accidentally.
Here's a sample connected page:
import React from 'react';
import {connect} from 'react-redux';
export const ConnectPage = (props) => {
return <div>{props.code}</div>;
};
export default connect(
(state) => ({}),
(dispatch) => ({})
)(ConnectPage);
Main Page:
import React from 'react';
import LoginPage from './LoginPage';
const MainPage = () => (
<LoginPage/>
);
export default MainPage;
Here's my routes file:
import React from 'react';
import {Route, Switch, Redirect} from 'react-router-dom';
import qs from 'query-string';
import MainPage from './MainPage';
import ConnectPage from './ConnectPage';
class Routes extends React.Component {
render() {
return (<Switch>
<Route path={'/connect/:provider'} render={(matches) => {
const search = qs.parse(matches.location.search);
if (search.code) {
return <ConnectPage code={search.code}/>;
}
return <Redirect to={'/'}/>;
}}/>
<Route exact path={'/'} component={MainPage}/>
</Switch>
);
}
}
export default Routes;
And here's my test that I'm having troubles with:
import React from 'react';
import {mount} from 'enzyme';
import {MemoryRouter} from 'react-router';
import configureStore from 'redux-mock-store';
import Chance from 'chance';
import Routes from './Routes';
import MainPage from './MainPage';
import ConnectPage from './ConnectPage';
import {createProvider as Provider} from 'react-redux';
const chance = new Chance();
const middlewares = [];
let mockStore;
describe('The Routes', () => {
beforeAll(()=>{
mockStore = configureStore(middlewares);
});
it('loads the Main Page on /', () => {
const component = mount(
<MemoryRouter initialEntries={['/']} initialIndex={0}>
<Routes store={mockStore}/>
</MemoryRouter>
);
expect(component.find(MainPage).length).toBe(1);
});
it('redirects to the main page if no code', () => {
const url = '/connect/someprovider';
const component = mount(
<MemoryRouter initialEntries={[url]} initialIndex={0}>
<Routes/>
</MemoryRouter>
);
expect(component.find(MainPage).length).toBe(1);
});
it('loads the Connect Page on /connect/provider', () => {
const code = chance.word({length: 32});
const url = `/connect/provider?code=${code}`;
const component = mount(
<Provider store={mockStore}>
<MemoryRouter initialEntries={[url]} initialIndex={0}>
<Routes/>
</MemoryRouter>
</Provider>);
try {
expect(component.find(ConnectPage).length).toBe(1);
} catch (e) {
console.log('Used url:', url);
throw (e);
}
});
});
I can't for the life of me figure out how to make the 3rd test pass. I would just want to make sure that the ConnectedPage gets put in place when it's appropriate.
In this state, I get:
Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
in createProvider (created by WrapperComponent)
in WrapperComponent

I have found an answer in the meantime, so I'll post it here for prosperity. If you know anything better, please let me know!
Here's the revised test case:
it('loads the Connect Page on /connect/provider', () => {
const code = chance.word({length: 32});
const url = `/connect/provider?code=${code}`;
const component = mount(
<MemoryRouter initialEntries={[url]} initialIndex={0}>
<Routes/>
</MemoryRouter>,
{
context: {
store: mockStore()
},
childContextTypes: {
store: PropTypes.object.isRequired
}
});
try {
expect(component.find(ConnectPage).length).toBe(1);
} catch (e) {
console.log('Used url:', url);
throw (e);
}
});
Calling the mockStore seemed to be the main solution instead of just passing it as is in the context. I'm not sure why this works, but it does. I'll roll with it for now, but would love to hear some explanations so that I can learn.

Related

React Redux prop is undefined

Beginner question.
I want to pass a user object to a component from store as a prop, but the component doesn't get it (undefined). I get the user object from a third party service authentication service (google firebase)
The middleware actually logs out in the console that the action of type SET_CURRENT_USER takes place, and next state indeed will have a user.currentUser set to the object returned from the login service (NOT UNDEFINED).
However, the component doesn't re-render and doesn't seem to receive the object as prop
The component, in which the prop is undefined
import React from 'react';
import { connect } from 'react-redux';
import { auth } from "../../firebase/firebase.utils";
export const Navbar = ({ currentUser }) => {
return (
/* A LOT OF JSX CODE. currentUser IS UNDEFINED */
);
};
const mapStateToProps = state => ({
currentUser: state.user.currentUser
});
export default connect(mapStateToProps)(Navbar);
The App component, which has the above component as a child. Also, I'm trying to set the store to contain the user object in the componentDidMount()
import React from 'react';
import Homepage from "./pages/homepage";
import { Route, Switch } from 'react-router-dom';
import { connect } from 'react-redux';
import Login from "./pages/login";
import Register from "./pages/register";
import { Navbar } from "./components/navbar/navbar";
import { auth } from "./firebase/firebase.utils";
import { setCurrentUser } from "./redux/user/user.actions";
class App extends React.Component {
unsubscribeFromAuth = null;
componentDidMount() {
this.unsubscribeFromAuth = auth.onAuthStateChanged(async userAuth => {
if(userAuth) {
(async () => {
const rawResponse = await fetch(/* JUST AN ASYNC FUNCTION TO POST TO BACKEND*/);
})();
}
this.props.setCurrentUser(userAuth); /*HERE IM TRYING TO SET THE STORE*/
})
}
componentWillUnmount() {
this.unsubscribeFromAuth();
}
render() {
return (
<div>
<Navbar /> /* THE COMPONENT WHICH SHOULD GET THE USER OBJECT AS PROP */
<Switch>
<Route exact={true} path={'/register'} component={Register} />
<Route exact={true} path={'/login'} component={Login} />
<Route path={'/'} component={Homepage} />
</Switch>
</div>
);
}
}
const mapDispatchToProps = (dispatch) => ({
setCurrentUser: user => dispatch(setCurrentUser(user))
});
export default connect(null, mapDispatchToProps)(App);
The index component
import React from 'react';
import ReactDOM from 'react-dom';
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import { Provider } from 'react-redux';
import store from "./redux/store";
ReactDOM.render(
<Provider store={store} > /* HERE IS STORE PROVIDED FROM IMPORT*/
<BrowserRouter>
<App/>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
Root reducer
import { combineReducers } from "redux";
export default combineReducers({
user: userReducer
});
User reducer
const INITIAL_STATE = {
currentUser: null
};
const userReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'SET_CURRENT_USER':
return {
...state,
currentUser: action.payload
};
default:
return state;
}
};
export default userReducer;
User action
export const setCurrentUser = user => ({
type: 'SET_CURRENT_USER',
payload: user
});
The store
import { createStore, applyMiddleware } from "redux";
import logger from 'redux-logger';
import rootReducer from './root-reducer';
const middlewares = [logger];
const store = createStore(rootReducer, applyMiddleware(...middlewares));
export default store;
You're doing both a named and default export for Navbar. The default export gets wrapped by the connect HOC that adds currentUser to its props. The named export does not.
You import it named like this: import { Navbar } from. Instead use the default export: import Navbar from.
Then I would suggest removing the named export to avoid future confusion.

Redux not working in a particularly Route after Refresh only

I am using React-Redux , React-Router , Saga , React-Thunk
All my application works well , one component which also dispatch action to be rendered gives me an error whenever I refresh the page OR type the url myself its a component that renders only when a photo clicked it display the particular photo after dispatching and filtering the state by the id which I send in the url , It works fine when I click the photo but after one refresh the state returns an empty array to be rendered here is my code
import React,{useEffect} from 'react';
import styled from 'styled-components';
import {connect} from 'react-redux';
import {fetchImages} from '../redux/actions/Actions';
const Photos = props => {
useEffect(()=>{props.fetchRequestedPhoto()},[props.requestedPhoto.length])
console.log(props.requestedPhoto)
}
const mapStateToProps = state =>( {
requestedPhoto:state.images
})
const mapDispatchToProps = () =>( {
fetchRequestedPhoto:()=>{return fetchImages()}
})
export default connect(mapStateToProps,mapDispatchToProps)(Photos);
navigation.js
import React from 'react'
import {BrowserRouter as Router , Route } from 'react-router-dom'
import HomePage from '../components/home'
import Categories from '../components/categories'
import Photos from '../components/photos.js'
import Category from '../components/category'
const Routes = () => {
return(
<Router>
<Route exact path='/' component={HomePage}></Route>
<Route exact path='/categories' component={Categories }></Route>
<Route exact path='/category/:category_id' component={Category}></Route>
<Route exact path='/photo/:photo_id' component={Photos}></Route>
</Router>
)
}
export default Routes;
Action.js
import axios from 'axios';
import { GetCategories,GetImages } from './actionTypes'
export function fetchCategories() {
var url = 'http://localhost:4000/all_categories';
return (dispatch) => {
return axios.get(url).then((res) => {
dispatch({
type: GetCategories,
payload: res.data
})
})
}
}
export function fetchImages(){
var url ='http://localhost:4000/all_images';
return(dispatch) => {
return axios.get(url).then((res)=>{
dispatch({
type:GetImages,
payload:res.data
})
})
}
}
saga.js
import {takeEvery,delay,put} from 'redux-saga/effects'
import axios from 'axios';
function* getImagesAsync(){
delay(4000)
const response=axios.get('http://localhost:4000/all_images');
const payloadImages = response;
yield put({type: 'GetImagesSaga', payload:payloadImages});
}
export function* watchFetchImages(){
yield takeEvery("GetImages",getImagesAsync);
}
Reducer.js
import { GetCategories , GetImages } from "../redux/actions/actionTypes"
const initialState = {
categories: [],
images:[]
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case GetCategories:
return {...state, categories:action.payload}
case GetImages:
return{...state,images:action.payload}
default:
return state;
}
};
export default rootReducer;
Any one could help me with this??

Using Redux without React to export functions

I want to be able to create a list of functions that are connected to Redux without using any React so that I can use these functions in various components across my app. How can I do this without using React / react components? Please see the below for my approach. Am I able to use react-redux's connect? Thanks.
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {fetchUsers} from './actions';
class Sample {
getUsers() {
this.props.fetchUsers();
}
displayUser() {
if (this.props.isSignedIn) {
return (
<h1>`The user ID is: ${this.props.userId}`</h1>
);
}
}
}
Sample.propTypes = {
isSignedIn: PropTypes.bool.isRequired,
userId: PropTypes.number.isRequired,
fetchUsers: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => ({
isSignedIn: state.isSignedIn,
userId: state.userId,
});
const mapDispatchToProps = (dispatch) => ({
fetchUsers: () => {
dispatch(fetchUsers());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Sample);
You can also see here for my index.js
import React from 'react';
import {Provider} from 'react-redux';
import {ConnectedRouter} from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';
import App from './app';
const history = createHistory();
const store = configureStore(history);
render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>, document.getElementById('app'),
);

reducer switch statements not working

I've just started out with Redux and trying to implement a simple MERN App (for practice).
Everything in my code is working fine, but my reducer function is showing unexpected behaviour. When an action (which gets fetches data from express api) is called my reducer correctly goes to the particular switch case data logs successfully but then three times the default case is passed and data on my component which I log is showing null. Please Help.
Here's my code:-
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import articlesReducer from './store/reducers/articlesReducer';
import registerServiceWorker from './registerServiceWorker';
const rootReducer = combineReducers({
articles: articlesReducer
});
const store = createStore(rootReducer, applyMiddleware(thunk));
const app = (
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>
);
ReactDOM.render(app, document.getElementById('root'));
registerServiceWorker();
App.js
import React, {Component} from 'react';
import {Switch, Route} from 'react-router-dom';
import './App.css';
import Home from './components/Home/Home';
class App extends Component {
render() {
return (
<div>
<Switch>
<Route exact path="/" component={Home} />
</Switch>
</div>
);
}
}
export default App;
articles.js
export const getAllArticles = () => {
return dispatch => {
return (
fetch('http://localhost:5000/api/articles')
.then(res => res.json())
.then(data => {
dispatch({type: 'GET_ALL_ARTICLES', articles: data})
})
);
};
};
articlesReducer.js
const initialState = {
articles:null
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'GET_ALL_ARTICLES':
console.log('in reducer', action.type, action.articles[0]);
return {
...state,
articles: action.articles
};
default:
console.log('In default');
return state;
}
};
export default reducer;
myComponent
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getAllArticles } from '../../store/actions/articles.js';
class MainPage extends Component {
componentWillMount() {
this.props.initArticles();
console.log(this.props.articles);
}
render() {
return (
<div className="container">
<br />
<h1>Here comes the articles!!</h1>
</div>
);
}
}
const mapStateToProps = state => {
return {
articles: state.articles
};
};
const mapDispatchToProps = dispatch => {
return {
initArticles: () => dispatch(getAllArticles())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(MainPage);
The output in my console is somewhat like this:-
In default
In default
In default
{articles: null}
in reducer GET_ALL_ARTICLES {articles[0] Object}
I don't know what is the mistake. Thanks for help in advance.
I'm not sure whether this is actually the problem but you incorrectly access the articles. You have a root reducer with articles reducer:
const rootReducer = combineReducers({
articles: articlesReducer
});
which initial state is:
const initialState = {
articles:null
};
And in your mapDispatchToProps you "import" whole reducer state:
const mapStateToProps = state => {
return {
articles: state.articles
};
};
I think you wanted to access articles property
const mapStateToProps = state => {
return {
articles: state.articles.articles
};
};
Other than that everything seems to be fine. I would however as pointed in comment initialize articles as empty array [].
const initialState = {
articles: []
};

ReactJs/Redux Invariant Violation: Could not find "store" in either the context or props of "Connect(LoginContainer)"

Not sure why I'm getting this error, it happened when I added connect from redux to my Login component, so I could connect my store.
FAIL src/components/auth/Login.test.js
● Test suite failed to run
Invariant Violation: Could not find "store" in either the context or props of "Connect(LoginContainer)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(LoginContainer)".
Index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from "react-redux"
import { createCommonStore } from "./store";
import App from './App'
import css from './manage2.scss'
const store = createCommonStore();
const element = document.getElementById('manage2');
console.log("Index.js Default store", store.getState());
ReactDOM.render(
<Provider store={store}> // <-- store added here
<App />
</Provider>, element);
store.js
import React from "react"
import { applyMiddleware, combineReducers, compose, createStore} from "redux"
import thunk from "redux-thunk"
import { userReducer } from "./reducers/UserReducer"
import { authReducer } from "./reducers/AuthReducer"
export const createCommonStore = (trackStore=false) => {
const reducers = combineReducers({
user: userReducer,
user: authReducer
});
//noinspection JSUnresolvedVariable
const store = createStore(reducers,
compose(
applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
if (trackStore) {
store.subscribe((() => {
console.log(" store changed", store.getState());
}));
}
return store;
};
App.js
import React from 'react'
import { BrowserRouter as Router } from 'react-router-dom'
import Routes from './components/Routes'
const supportsHistory = "pushState" in window.history
export default class App extends React.Component {
render() {
return (
<Router forceRefresh={!supportsHistory}>
<Routes />
</Router>
);
}
}
Routes.js
import React from 'react'
import { Route, Switch } from 'react-router-dom'
import LoginContainer from './auth/Login'
import Dashboard from './Dashboard'
import NoMatch from './NoMatch'
const Routes = () => {
return (
<Switch>
<Route exact={ true } path="/" component={ LoginContainer }/>
<Route path="/dashboard" component={ Dashboard }/>
<Route component={ NoMatch } />
</Switch>
);
}
export default Routes
Finally Login.js (code removed for brevity
import React from 'react'
import { connect } from "react-redux"
import { bindActionCreators } from 'redux';
import { setCurrentUser } from '../../actions/authActions'
import * as api from '../../services/api'
const mapDispatchToProps = (dispatch) => {
console.log('mapDispatchToProps', dispatch);
return {
setUser: (user) => {
bindActionCreators(setCurrentUser(user), dispatch)
}
}
}
class LoginContainer extends React.Component {
constructor(props) {
super(props)
this.state = {};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(e) {
}
handleSubmit(e) {
}
render() {
return (
<div className="app-bg">
...
</div>
)
}
}
export default connect(null, mapDispatchToProps)(LoginContainer);
Login.test
import React from 'react'
import ReactTestUtils from 'react-dom/test-utils'
import { mount, shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { missingLogin } from '../../consts/errors'
import Login from './Login'
import Notification from '../common/Notification'
const loginComponent = shallow(<Login />);
const fakeEvent = { preventDefault: () => '' };
describe('<Login /> component', () => {
it('should render', () => {
const tree = toJson(loginComponent);
expect(tree).toMatchSnapshot();
});
it('should render the Notification component if state.error is true', () => {
loginComponent.setState({ error: true });
expect(loginComponent.find(Notification).length).toBe(1);
});
});
describe('User Login', () => {
it('should fail if no credentials are provided', () => {
expect(loginComponent.find('.form-login').length).toBe(1);
loginComponent.find('.form-login').simulate('submit', fakeEvent);
expect(loginComponent.find(Notification).length).toBe(1);
const notificationComponent = shallow(<Notification message={ missingLogin }/>);
expect(notificationComponent.text()).toEqual('Please fill out both username and password.');
});
it('input fields should be filled correctly', () => {
const credentials = { username: 'leongaban', password: 'testpass' };
expect(loginComponent.find('#input-auth-username').length).toBe(1);
const usernameInput = loginComponent.find('#input-auth-username');
usernameInput.value = credentials.username;
expect(usernameInput.value).toBe('leongaban');
const passwordInput = loginComponent.find('#input-auth-password');
passwordInput.value = credentials.password;
expect(passwordInput.value).toBe('testpass');
});
});
What do you see wrong here?
Redux recommends exporting the unconnected component for unit tests. See their docs.
In login.js:
// Named export for tests
export class LoginContainer extends React.Component {
}
// Default export
export default connect(null, mapDispatchToProps)(LoginContainer);
And in your test:
// Import the named export, which has not gone through the connect function
import { LoginContainer as Login } from './Login';
You can then specify any props that would have come from the store directly on the component.
You need to pass store as either a prop or context in your test. mount method accepts context as another parameter.
and how do you get store here? You create store the same way you created in app.js
You could use React's contextType or pass propType. You would need to declare it either as a prop or contextType.
Provider.contextTypes = {
Store: React.PropTypes.object.isRequired
};
Provider.propTypes= {
Store: React.PropTypes.object.isRequired
};

Categories

Resources