Tests React component with jest and enzyme - javascript

i have components presented below. I am totally new in unit testing. Can anyone give any one give me advice how and what should I test in this component? I was trying to shallow render it, to check is text in h2 is present but i still getting errors.
import React, { useEffect } from 'react';
import { Form, Field } from 'react-final-form';
import { useHistory, Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { loginUser, clearErrorMessage } from '../../redux/auth/authActions';
import Input from '../Input/Input';
import ROUTES from '../../routes/routes';
import './LoginForm.scss';
const LoginForm = () => {
const dispatch = useDispatch();
const history = useHistory();
const { loading, isLogged, errorMessage } = useSelector(state => state.auth);
useEffect(() => {
if (isLogged) {
history.push('/');
}
return () => {
dispatch(clearErrorMessage());
};
}, [dispatch, history, isLogged]);
const handleSubmitLoginForm = values => {
if (!loading) {
dispatch(loginUser(values));
}
};
const validate = ({ password }) => {
const errors = {};
if (!password) {
errors.password = 'Enter password!';
}
return errors;
};
return (
<article className="login-form-wrapper">
<h2>SIGN IN</h2>
<Form onSubmit={handleSubmitLoginForm} validate={validate}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit} autoComplete="off" className="login-form">
<div className="login-form__field">
<Field name="email" component={Input} type="email" label="E-mail" />
</div>
<div className="login-form__buttons">
<button type="submit" className={loading ? 'button-disabled' : ''}>
Sign in
</button>
</div>
</form>
)}
</Form>
</article>
);
};
export default LoginForm;
I am open for any advices :)

First of all, I am not recommending using shallow in your tests and here is a great article why.
I also recommend to check out react-testing-library instead of Enzyme as it is much nicer to use.
Now, to answer your question. You are using here hooks for redux and react-router, so you need to provide the necessary data to your componenent in test so that it can use those hooks. Let me show you an example test (that checks text in h2 element):
import React from 'react';
import { mount } from 'enzyme';
import {Provider} from 'react-redux';
import {MemoryRouter, Route} from 'react-router';
import LoginForm from './LoginForm';
describe('Login Form', () => {
it('should have SIGN IN header', () => {
const store = createStore();
const component = mount(
<Provider store={store}>
<MemoryRouter initialEntries={['/login']}>
<Route path="/:botId" component={LoginForm} />
</MemoryRouter>
</Provider>
)
expect(component.find('h2').text()).toEqual('SIGN IN');
});
});
Some explanation to this example.
I am using mount instead of shallow as I prefer to render as much as possible in my test, so that I can check if everything works together as it should.
You can see that I am not rendering my component directly, but rather it is wrapped with other components (Provider from react-redux and MemoryRouter from react-router). Why? Because I need to provide context to my Component. In this case it's redux and router context so that the data used inside exists and can be found (for example useSelector(state => state.auth) must have some state provided so that it can access auth property). If you remove any of them you would get some error saying that this context is missing - go ahead and check for yourself :).
See here for some details around testing with router context
As for testing with redux in my example there is a createStore function that I didn't define as there are a few approaches to this. One involves creating a real store that you use in your production application. This is the one that I prefer and colleague of mine wrote great article around this topic here. Other is to create some kind of mock store, like in this article. Again, I prefer the first approach, but whichever is better for you.
Answering your other question on what should you test in this example. There are multiple possibilities. It all depends mostly on you business case, but examples that I would test here includes:
Typing something into an input, clicking a button and observing that login is successful (by redirecting to new path - / in this case)
not typing a password and clicking a button - error should be shown
Checking if button class changes when it's loading
Do not dispatch login action twice, when already loading
And so on...
That's really just a tip of an iceberg on what could be written around testing, but I hope it helps and gives you a nice start to dig deeper into the topic.

Related

Finding the buttons on the screen that have no text for the test

I am trying to write the tests for the NavBar component (using react-native-testing-library) that has several buttons that are basically just icons (using ui-kitten for react native). So I can't get these buttons by text (as there is none) but other methods didn't work for me either (like adding accesibilityLabel or testID and then getting by the label text / getting by test ID). Any ideas what I am doing wrong?
// NavBar.tsx
import React from 'react';
import {View, StyleSheet} from 'react-native';
import {HomeBtn, SaveBtn} from '../components/buttons';
import UserSignOut from './UserSignOut';
const NavBar = ({
navigation,
pressHandlers,
}) => {
return (
<View style={styles.navBar}>
<View>
<HomeBtn navigation={navigation} />
<SaveBtn pressHandler={pressHandlers?.saveBtn ?? undefined} />
</View>
<UserSignOut />
</View>
);
};
export default NavBar;
// HomeBtn.tsx
import React from 'react';
import {Button} from '#ui-kitten/components';
import {HomeIcon} from '../shared/icons';
import styles from './Btn.style';
export const HomeBtn = ({navigation}: any) => {
return (
<Button
accesibilityLabel="home button"
style={styles.button}
accessoryLeft={props => HomeIcon(props, styles.icon)}
onPress={() => navigation.navigate('Home')}
/>
);
};
// NavBar.test.tsx
import React from 'react';
import {render, screen} from '#testing-library/react-native';
import * as eva from '#eva-design/eva';
import {RootSiblingParent} from 'react-native-root-siblings';
import {EvaIconsPack} from '#ui-kitten/eva-icons';
import {ApplicationProvider, IconRegistry} from '#ui-kitten/components';
import NavBar from '../../containers/NavBar';
describe('NavBar', () => {
const navBarContainer = (
<RootSiblingParent>
<IconRegistry icons={EvaIconsPack} />
<ApplicationProvider {...eva} theme={eva.light}>
<NavBar />
</ApplicationProvider>
</RootSiblingParent>
);
it('should render the buttons', async () => {
render(navBarContainer);
// this test fails (nothing is found with this accesibility label)
await screen.findByLabelText('home button');
});
});
Query predicate
The recommended solution would be to use:
getByRole('button', { name: "home button" })
As it will require both the button role, as well as check accessibilityLabel with name option.
Alternative, but slightly less expressive way would be to use:
getByLabelText('home button')
This query will only check accessibilityLabel prop, which also should work fine.
Why is query not matching
Since you're asking why the query is not working, that depends on your test setup. It seems that you should be able to use sync getBy* query and do not need to await findBy* query, as the HomeBtn should be rendered without waiting for any async action.
What might prevent that test from working could be incorrect mocking of any of the wrapping components: RootSiblingParent, ApplicationProvider, they might be "consuming" children prop without rendering it. In order to diagnose the issue you can use debug() function from RNTL to inspect the current state of rendered components. You can also run your tests on render(<NavBar />) to verify that.
Does await screen.findByA11yLabel('home button') work? It should match the accessibilityLabel prop.

Using client-only routes with page templates coming from Contentful

Goal
I am looking to use client-only routes for content under a certain URL (/dashboard). Some of this content will be coming from Contentful and using a page template. An example of this route would be {MYDOMAIN}/dashboard/{SLUG_FROM_CONTENTFUL}. The purpose of this is to ensure projects I have worked on at an agency are not able to be crawled/accessed and are only visible to 'employers' once logged in.
What I have tried
My pages are generated via gatsby-node.js. The way of adding authentication/client-only routes has been taken from this example. Now the basics of it have been setup and working fine, from what I can tell. But the private routes seem to only work in the following cases:
If I'm logged in and navigate to /dashboard
I'm shown Profile.js
If I an not logged in and go to /dashboard
I'm shown Login.js
So that all seems to be fine. The issue comes about when I go to /dashboard/url-from-contentful and I am not logged in. I am served the page instead of being sent to /dashboard/login.
exports.createPages = async ({graphql, actions}) => {
const { createPage } = actions;
const { data } = await graphql(`
query {
agency: allContentfulAgency {
edges {
node {
slug
}
}
}
}
`);
data.agency.edges.forEach(({ node }) => {
createPage({
path: `dashboard/${node.slug}`,
component: path.resolve("src/templates/agency-template.js"),
context: {
slug: node.slug,
},
});
});
}
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions;
if(page.path.match(/^\/dashboard/)) {
page.matchPath = "/dashboard/*";
createPage(page);
}
};
My auth.js is setup (the username and password are basic as I am still only developing this locally):
export const isBrowser = () => typeof window !== "undefined";
export const getUser = () =>
isBrowser() && window.localStorage.getItem("gatsbyUser")
? JSON.parse(window.localStorage.getItem("gatsbyUser"))
: {};
const setUser = (user) =>
window.localStorage.setItem("gatsbyUser", JSON.stringify(user));
export const handleLogin = ({ username, password }) => {
if (username === `john` && password === `pass`) {
return setUser({
username: `john`,
name: `Johnny`,
email: `johnny#example.org`,
});
}
return false;
};
export const isLoggedIn = () => {
const user = getUser();
return !!user.username;
};
export const logout = (callback) => {
setUser({});
call
};
PrivateRoute.js is setup the following way:
import React from "react";
import { navigate } from "gatsby";
import { isLoggedIn } from "../services/auth";
const PrivateRoute = ({ component: Component, location, ...rest }) => {
if (!isLoggedIn() && location.pathname !== `/dashboard/login`) {
navigate("/dashboard/login");
return null;
}
return <Component {...rest} />;
};
export default PrivateRoute;
dashboard.js has the following. The line <PrivateRoute path="/dashboard/url-from-contentful" component={Agency} />, I have tried a couple of things here - Statically typing the route and using the exact prop, using route parameters such as /:id, /:path, /:slug :
import React from "react";
import { Router } from "#reach/router";
import Layout from "../components/Layout";
import Profile from "../components/Profile";
import Login from "../components/Login";
import PrivateRoute from "../components/PrivateRoute";
import Agency from "../templates/agency-template";
const App = () => (
<Layout>
<Router>
<PrivateRoute path="/dashboard/url-from-contentful" component={Agency} />
<PrivateRoute path="/dashboard/profile" component={Profile} />
<PrivateRoute path="/dashboard" />
<Login path="/dashboard/login" />
</Router>
</Layout>
);
export default App;
And finally agency-template.js
import React from "react";
import { graphql, Link } from "gatsby";
import styled from "styled-components";
import SEO from "../components/SEO";
import Layout from "../components/Layout";
import Gallery from "../components/Gallery";
import GeneralContent from "../components/GeneralContent/GeneralContent";
const agencyTemplate = ({ data }) => {
const {
name,
excerpt,
richDescription,
richDescription: { raw },
images,
technology,
website,
} = data.agency;
const [mainImage, ...projectImages] = images;
return (
<>
<SEO title={name} description={excerpt} />
<Layout>
<div className="container__body">
<GeneralContent title={name} />
<Gallery mainImage={mainImage} />
<GeneralContent title="Project Details" content={richDescription} />
<div className="standard__images">
<Gallery projectImages={projectImages} />
</div>
<ViewWebsite>
<Link className="btn" to={website}>
View the website
</Link>
</ViewWebsite>
</div>
</Layout>
</>
);
};
export const query = graphql`
query ($slug: String!) {
agency: contentfulAgency(slug: { eq: $slug }) {
name
excerpt
technology
website
images {
description
gatsbyImageData(
layout: FULL_WIDTH
placeholder: TRACED_SVG
formats: [AUTO, WEBP]
quality: 90
)
}
richDescription {
raw
}
}
}
`;
export default agencyTemplate;
I assume that gating content from a CMS is possible with Gatsby but I might be wrong given it is an SSG. I may be misunderstanding the fundamentals of client-only. The concepts in React and using Gatsby are still very new to me so any help or guidance in achieving the goal would be appreciated.
What I ended up doing
So the answer I marked was the one that 'got the ball rolling'. The explanation of what was happening with state and requiring either useContext or redux helped me understand where I was going wrong.
Also, the suggestion to use web tokens prompted me to find more information on using Auth0 with the application.
Once I had got out of the mindset of creating pages using Gatsby (Through a template, via gatsby-node.s), and instead doing it in a 'React way' (I know Gatsby is built with React) by handling the routing and GraphQL it became clearer. Along with the authentication, all I ended up doing was creating a new <Agency /> component and feeding the data from GraphQL into it and updating the path with my map().
return (
<>
<Router>
<DashboardArea path="/dashboard/" user={user} />
{agencyData.map(({ node }, index) =>
node.slug ? (
<Agency key={index} data={node} path={`/dashboard/${node.slug}`} />
) : null
)}
</Router>
</>
);
I assume that in your PrivateRoute component, you're using the isLoggedIn check incorrectly. importing and using isLoggedIn from auth.js will run only initially and will not act as a listner. What you can do is that store the value of isLoggedin in global state variable like(useContext or redux) and make a custom hook to check for the login state. Secondly avoid accessing localStorage directly, instead use the global state managment (useContext, redux) or local state managment (useState, this.state).
Note: that when ever you go to a route by directly pasting url in browser, it always refreshes the page and all your stored state is reinitialized. This may be the reason why you may be experiencing this issue. The browser does not know that you had been previously logged in and therefore it always validates once your application is mounted. What you can do is that you can store isLoggedIn state in browser's localstore. Personally I like to use redux-persist for that.
export const useGetUser = () => { //add use infront to make a custom hook
return useSelector(state => state.gatsByUser) // access user info from redux store
};
export const handleLogin = ({ username, password }) => {
//suggestion: don't validate password on client side or simply don't use password,
//instead use tokens for validation on client side
if (username === `john` && password === `pass`) {
dispatch(setUserInfo({
username: `john`,
name: `Johnny`,
email: `johnny#example.org`,
isLoggedIn: true,
}));
return true;
}
return false;
};
// adding 'use' infront to make it a custom hook
export const useIsLoggedIn = () => {
//this will act as a listner when ever the state changes
return useSelector(state => state.gatsByUser?.isLoggedIn ?? false);
};
export const logout = (callback) => {
const dispatch = useDispatch(); // redux
dispatch(clearUserInfo());
};
Now in private route do
import React from "react";
import { navigate } from "gatsby";
import { useIsLoggedIn } from "../services/auth";
const PrivateRoute = ({ component: Component, location, ...rest }) => {
const isLoggedIn = useIsLoggedIn();
if (!isLoggedIn) {
return navigate("/dashboard/login");
}
return <Component {...rest} />;
};
export default PrivateRoute;
It looks like you're server-side rendering dashboard/[url] in gatsby-node.js/createPages()? IIRC those routes will have higher precedence than dynamic routes (which you specify with #reach/router in dashboard.js).
Plus, the content of those routes are currently publicly available. If you want to keep them truly private, you should query Contentful graphql API directly on the client side (via fetch() or use apollo client, urql, etc.), instead of relying on Gatsby's graphql server.
I would do the follows:
Removing the dashboard/[url] portion in your gatsby-node.js
Configure your web host so that all routes matches '/dashboard/*' will redirect to '/dashboard'
If you happen to host your static site on Netlify, you'd create a _redirects with this, assuming you configure Gatsby to create nice url:
# /static/_redirect
/dashboard/* /dashboard 200
A possible simpler way that match your current setup is gating content at web host level. You can configure nginx to protect /dasboard/* with basic auth. However maintaining/updating password is a pain & modern hosting solution don't really allow user to configure that.
Netlify offers its own authentication solution that you could look into.
I've had the same issue earlier and I couldn't get exact functionality with Private Routes.
In my case, I created two separate Layouts for Public and Private Routes and built the authentication to Private Layout. Logged-in user data were linked to a redux store (First I used Context, then moved to Redux). In Private routes with the Private Layout, it redirected the guest users to the Login page and redirected them to the same page after login.
Private layout is something like this:
import React from "react";
import { navigate } from "gatsby";
import { useSelector } from "react-redux";
const PrivateLayout = ({children}) => {
const isLoggedIn = useSelector(state => state.user.isLoggedIn);
useEffect(() => {
if (!isLoggedIn) {
// redirect the user to login page.
// I'm sending the current page's URL as the redirect URL
// so that I can take the user back to this page after logging in.
}
}, [isLoggedIn])
if (!isLoggedIn) return null;
return <>
{...header}
{children}
{...footer}
</>
}
export default PrivateLayout;
Not sure if this workaround suits you. If it does, I can give you more info.

How to communicate between React components which do not share a parent?

I am adding React to an already existing front end and am unsure how to communicate data between components.
I have a basic text Input component and a Span component, mounted separately. When the user types into the input, I want the text of the span to change to what is input.
Previously I would start a React project from scratch and so have the Input and Span share an App component as a parent. I'd use a prop function to lift the text state from the Input to the App and pass it down the value to the Span as a prop. But from scratch is not an option here.
I've considered:
Redux etc. As I'm introducing React piece by piece to this project and some team members have no React experience, I want to avoid using Redux or other state management libraries until very necessary, and it seems overkill for this simple case.
React Context API. This doesn't seem correct either, as my understanding was that context API should be kept for global data like "current authenticated user, theme, or preferred language" shared over many components, not just for sharing state between 2 components.
UseEffect hook. Using this hook to set the inner HTML of the Span component i.e
function Input() {
const inputProps = useInput("");
useEffect(() => {
document.getElementsByClassName('page-title')[0].innerHTML = inputProps.value;
})
return (
<div>
<h3>Name this page</h3>
<input
placeholder="Type here"
{...inputProps}
/>
</div>
);
}
Which sort of negates the whole point of using React for the Span?
I've gone with the UseEffect hook for now but haven't found any clear answers in the React docs or elsewhere online so any advice would be helpful.
Thanks.
Input.jsx
import React, { useState, useEffect } from 'react';
function useInput(defaultValue) {
const [value, setValue] = useState(defaultValue);
function onChange(e) {
setValue(e.target.value);
}
return {
value,
onChange
}
}
function Input() {
const inputProps = useInput("");
useEffect(() => {
document.getElementsByClassName('page-title')[0].innerHTML = inputProps.value;
})
return (
<div>
<h3>React asks what shall we name this product?</h3>
<input
placeholder="Type here"
{...inputProps}
/>
</div>
);
}
export default Input;
PageTitle.jsx
import React from 'react';
function PageTitle(props) {
var title = "Welcome!"
return (
<span>{props.title}</span>
)
}
;
export default PageTitle
Index.js
// Imports
const Main = () => (
<Input />
);
ReactDOM.render(
<Main />,
document.getElementById('react-app')
);
ReactDOM.render(
<PageTitle title="Welcome"/>,
document.getElementsByClassName('page-title')[0]
);
In React, data is supposed to flow in only one direction, from parent component to child component. Without getting into context/redux, this means keeping common state in a common ancestor of the components that need it and passing it down through props.
Your useEffect() idea isn't horrible as a kind of ad hoc solution, but I would not make PageTitle a react component, because setting the value imperatively from another component really breaks the react model.
I've used useEffect() to set things on elements that aren't in react, like the document title and body classes, as in the following code:
const siteVersion = /*value from somewhere else*/;
//...
useEffect(() => {
//put a class on body that identifies the site version
const $ = window.jQuery;
if(siteVersion && !$('body').hasClass(`site-version-${siteVersion}`)) {
$('body').addClass(`site-version-${siteVersion}`);
}
document.title = `Current Site: ${siteVersion}`;
}, [siteVersion]);
In your case, you can treat the span in a similar way, as something outside the scope of react.
Note that the second argument to useEffect() is a list of dependencies, so that useEffect() only runs whenever one or more changes.
Another side issue is that you need to guard against XSS (cross site scripting) attacks in code like this:
//setting innerHTML to an unencoded user value is dangerous
document.getElementsByClassName('page-title')[0].innerHTML = inputProps.value;
Edit:
If you want to be even more tidy and react-y, you could pass a function to your input component that sets the PageTitle:
const setPageTitle = (newTitle) => {
//TODO: fix XSS problem
document.getElementsByClassName('page-title')[0].innerHTML = newTitle;
};
ReactDOM.render(
<Main setPageTitle={setPageTitle} />,
document.getElementById('react-app')
);
//inside Main:
function Input({setPageTitle}) {
const inputProps = useInput("");
useEffect(() => {
setPageTitle(inputProps.value);
})
return (
<div>
<h3>React asks what shall we name this product?</h3>
<input
placeholder="Type here"
{...inputProps}
/>
</div>
);
}
You can create a HOC or use useContext hook instead

How to link to next page url in handleSubmit formik?

I have two functional components in my process. First, I need the first component info to be filled and validated by Formik and Yup and then user can process the next step in the second component by click Next. For now, I can get everything validated and code can reach on handleSubmit() without any problem. But, the problem is that, I could not link to another component using <Link>. I have tried:
// Using this first one, no validation is performed and it will link to another component directly
<Link to="/Next">
<button type="submit">Next</Button>
</Link>
// I have put these inside handleSubmit() but it is undefined.
this.context.router.push('/Next');
Router.push('/Next')
this.props.history.push('/Next')
Mostly I got undefined output on console using these code. It's seems like that i could not access props from functional components like i could in the react class. Here is my first component:
import React from 'react';
import { withFormik, Field } from 'formik';
import {
BrowserRouter as Router,
Route,
Link,
Switch,
Redirect
} from "react-router-dom";
const MyForm = props => { const {handleSubmit} = props;
return (
<form onSubmit={handleSubmit}>
<Field type="email" name="email" placeholder="Email" />
<button type="submit">Next</button>
// <Link to="/Next">
// <button type="submit">Next</Button>
// </Link>
</Field>
);
};
const MyEnhancedForm = withFormik({
mapPropsToValues: () => ({ email: '' }),
handleSubmit: (values, formikBag) => {
// Link to next page code
},
})(MyForm);
Yes we can't use this.props in functional components but what we can do is that write the same routing logic in parent component and pass it as a prop in the functional component.
Then we can call the same function in handleSubmit().
If you are using hooks you can follow the approach described in this blog post https://dev.to/httpjunkie/programmatically-redirect-in-react-with-react-router-and-hooks-3hej in order to programmatically redirect with react-router and hooks. A summary:
Include useState in your imports:
import React, { useState } from 'react';
Inside your functional component add:
const [toNext, setToNext] = useState(false)
Inside handleSubmit add:
setToNext(true)
Inside the <form> add:
{toNext ? <Redirect to="/Next" /> : null}

React event listeners based on changes in state made with Redux

Good afternoon,
I am having some difficulty working with React and Redux when I am trying to redirect users of my app based on changes in state.
At a high level: I want my app's route to change when my user object in state is populated with information from home / to /student/:username.
Right now I have accomplished this in a hacky sort of fashion.
In my Login component I use the componentDidUpdate lifecycle hook to listen and dispatch an action when an access token from a 3rd party API is passed back to the client from my Express server.
import React from "react";
import LoginForm from "../minor/LoginForm";
const Login = React.createClass({
componentDidUpdate(){
const {success, access_token} = this.props.loginStatus;
if (success === true && access_token !== null){
console.log("It worked getting your data now...");
this.props.getUserData(access_token);
} else if (success === false || access_token === null){
console.log("Something went wrong...");
}
},
render(){
return(
<div className="loginComponentWrapper">
<h1>Slots</h1>
<LoginForm loginSubmit={this.props.loginSubmit}
router={this.props.router}
user={this.props.user} />
New User?
</div>
)
}
});
Notice that I am passing router and user to my LoginForm component. I do this in order to use ANOTHER componentDidUpdate where I use the .push method on router like so:
import React from "react";
const LoginForm = React.createClass({
componentDidUpdate(){
const {router, user} = this.props;
if (user.username !== null){
router.push(`/student/${user.username}`);
}
},
render(){
return(
<div className="loginWrapper">
<div className="loginBox">
<form className="loginForm" action="">
<input ref={(input) => this.username_field = input} type="text" placeholder="username" defaultValue="kitties#kit.com" />
<input ref={(input) => this.password_field = input} type="text" placeholder="password" defaultValue="meowMeow3" />
<button onClick={this.loginAttempt}>Login</button>
</form>
</div>
</div>
)
}
});
Sure it works but I'm certain this is a overly complicated solution and not what is considered a best practice. I've tried adding a custom listener method within my Login component but I've never had it successfully fire off, instead my app gets stuck in a loop.
Is there a way I can do this using Redux and keep my components tidier, or take advantage of componentDidUpdate in a more efficient way?
As always I'd appreciate some more experienced eyes on my issue and look forward to some feedback!
Thanks
UPDATE
App.js
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as actionCreators from "../actions/userActions.js";
import StoreShell from "./StoreShell.js";
function mapStateToProps(state){
return{
loginStatus: state.loginStatus,
user: state.user
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators(actionCreators, dispatch)
}
const App = connect(mapStateToProps, mapDispatchToProps)(StoreShell);
export default App;
This component "sprinkles" all my redux stuff and state data into my container component named StoreShell that in turn passes all that data to props for the elements that make up the UI like Login and LoginForm am I taking too many steps?
StoreShell.js
import React from "react";
const StoreShell = React.createClass({
render(){
return(
<div className="theBigWrapper">
{React.cloneElement(this.props.children, this.props)}
</div>
)
}
})
export default StoreShell;
There are several things that could make the login flow easier to manage and reason about and tidy up your components a bit. First a few general points:
I'm not certain why you have divided login logic between the two components? The idiomatic React/Redux approach would be to have a container component that deals with logic, and a presentation component that deals with presentation. Currently both components do a little bit of each.
You don't need to pass props.router up and down through your components. React router provides a HOC that provides router as a props called withRouter( docs here). You just wrap a component in withRouter and props.router is available - everything else stays the same.
export default withRouter(LoginForm);
My personal feeling is that the URI is part of your app's state, and as such it should be maintained within the store and updated by dispatching actions. There is a cracking library available to do this - react-router-redux. Once you have it setup then you can pass the push method to your components (or elsewhere... see the next points) to navigate:
import { push } from 'react-router-redux';
import { connect } from 'react-redux';
const NavigatingComponent = props => (
<button onClick={() => props.push('/page')}>Navigate</button>
);
const mapStateToProps = null;
const mapDispatchToProps = {
push
};
export default connect(mapStateToProps, mapDispatchToProps)(NavigatingComponent);
One of the great things about having the URI in our store is that we don't need access to props.router to change location, which opens the avenue of moving the login logic outside of our components. There are several ways we can do this, the simplest is probably redux-thunk, which allows our action creators to return functions instead of objects. We could then write our component to simply call a login function with username and password entered, and our thunk takes care of the rest:
import { push } from 'react-router-redux';
// action creators....
const loginStarted = () => ({
type: 'LOGIN_STARTED'
)};
const loginFailed = (error) => ({
type: 'LOGIN_FAILED',
payload: {
error
}
};
const loginSucceeded = (user) => ({
type: 'LOGIN_SUCCEEDED',
payload: {
user
}
};
const getUserData = (access_token) => (dispatch) => {
Api.getUserData(access_token) // however you get user data
.then(response => {
dispatch(loginSucceeded(response.user));
dispatch(push(`/student/${response.user.username}`));
});
export const login = (username, password) => (dispatch) => {
dispatch(loginStarted());
Api.login({ username, password }) // however you call your backend
.then(response => {
if (response.success && response.access_token) {
getUserData(access_token)(dispatch);
} else {
dispatch(loginFailed(response.error));
}
});
}
The only thing your components do is call the initial login function, which could be implemented something like:
Container:
import React from 'react';
import { connect } from 'react-redux';
import { login } from '../path/to/actionCreators';
import LoginForm from './LoginForm';
const LoginContainer = React.createClass({
handleSubmit() {
this.props.login(
this.usernameInput.value,
this.passwordInput.value
);
},
setUsernameRef(input) {
this.usernameInput = input;
},
setPasswordRef(input) {
this.passwordInput = input;
},
render() {
return (
<LoginForm
handleSubmit={this.handleSubmit.bind(this)}
setUsernameRef={this.setUsernameRef.bind(this)}
setPasswordRef={this.setPasswordRef.bind(this)}
/>
);
}
});
const mapStateToProps = null;
const mapDispatchToProps = {
login
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer);
Component:
import React from 'react';
export const LoginForm = ({ handleSubmit, setUsernameRef, setPasswordRef }) => (
<div className="loginWrapper">
<div className="loginBox">
<form className="loginForm" action="">
<input ref={setUsernameRef} type="text" placeholder="username" defaultValue="kitties#kit.com" />
<input ref={setPasswordRef} type="text" placeholder="password" defaultValue="meowMeow3" />
<button onClick={handleSubmit}>Login</button>
</form>
</div>
</div>
);
This has achieved separation of a logic/data providing container, and a stateless presentational component. The LoginForm component can be written simply as a function above because it has no responsibility to deal with logic. The container is also a very simple component - the logic has all been isolated in our action creator/thunk and is much easier to read and reason about.
redux-thunk is just one way of managing asynchronous side effects with redux - there are many others with different approaches. My personal preference is toward redux-saga, which may be interesting for you to look at. In my own redux journey, however, I certainly benefited from using and understanding redux-thunk first before finding it's limitations/drawbacks and moving on, and would suggest this route to others.
If you're using react-router version 4.x.x: You can just render a Redirect component that handles the redirection for you (see example in react-router docs).
import React from "react";
import { Redirect } from "react-router";
import LoginForm from "../minor/LoginForm";
const Login = React.createClass({
componentDidUpdate(){
const {success, access_token} = this.props.loginStatus;
if (success === true && access_token !== null){
console.log("It worked getting your data now...");
this.props.getUserData(access_token);
} else if (success === false || access_token === null){
console.log("Something went wrong...");
}
}
render(){
// if user is defined, redirect to /student/:username
if (this.props.user.username !== null) {
return (<Redirect to={ `/student/${this.props.user.username}` } />)
}
// otherwise render the login form
return(
<div className="loginComponentWrapper">
<h1>Slots</h1>
<LoginForm loginSubmit={this.props.loginSubmit}
router={this.props.router}
user={this.props.user} />
New User?
</div>
)
}
});
If you're using react-router version 3.x.x: The the way you're doing it is mostly correct. I would move the redirect from the LoginForm component to Login component; that way LoginForm does not need to depend on the user prop.
I know, I don't like the pattern much either.
Hope this helps!

Categories

Resources