Skip to content

Instantly share code, notes, and snippets.

@allisonjw
Last active January 15, 2020 15:57
Show Gist options
  • Save allisonjw/8fd8c73bf3b3d63926a2beddeaadf253 to your computer and use it in GitHub Desktop.
Save allisonjw/8fd8c73bf3b3d63926a2beddeaadf253 to your computer and use it in GitHub Desktop.
React w/ Jest, Router, Redux

Original Sources

React I: The What and The Why https://frontend.turing.io/lessons/module-3/react-i.html

React II: The How, building IdeaBox https://frontend.turing.io/lessons/module-3/react-ii.html

React III: Workshop Ideabox With A Backend https://frontend.turing.io/lessons/module-3/react-iii.html

Unit Testing React Components https://frontend.turing.io/lessons/module-3/unit-testing-react.html

Get Your Site On GH Pages https://github.com/turingschool-examples/webpack-starter-kit/blob/master/gh-pages-procedure.md

Webpack Starter Kit https://github.com/turingschool-examples/webpack-starter-kit

Network Request GET/POST Requests https://frontend.turing.io/lessons/module-3/network-request-exercises.html

All Lessons https://frontend.turing.io/lessons/

Creating a React App

  1. In the terminal run: npx create-react-app NAME-OF-APP
  2. Cd into the new directory: cd NAME-OF-APP
  3. Run: npm install.
  4. You can run npm start to see if the app was set up correctly.

Setup Redux

  1. npm i redux react-redux redux-devtools-extension -S
  • redux - Allows us to have access to using Redux in our app.
  • react-redux - Allows us to connect our react components to our Redux store.
  • redux-devtools-extension - Useful for debugging in our devtools
  1. In src/index.js

import { Provider } from 'react-redux';

  • a component from react-redux that wraps our App component and allows each child component to be connected to the store

import { createStore } from 'redux';

  • a function from Redux that uses the rootReducer to create the store

import { composeWithDevTools } from 'redux-devtools-extension';

  • a method we brought in and can pass as an argument with createStore so that we have access to our devtools and can view our store.

import { rootReducer } from './reducers';

  • our combined reducers to create our store

(order matters here)

const store = createStore(rootReducer, composeWithDevTools());

ReactDOM.render(
  <Provider store={store} >
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>, 
  document.getElementById('root')
);
  1. In actions/index.js
export const addToDo = todo => ({
 type: 'ADD_TODO',
 todo
})
  1. In src/reducers/index.js
import { combineReducers } from 'redux';
import { todos } from './todos';

export const rootReducer = combineReducers({
  todos: todos
});
  1. Connecting components to the Redux store:
import { connect } from 'react-redux';

mapState example:

export const mapState = state => ({
  intentions: state.intentions
})

export default connect(mapState)(IntentionsContainer)

mapDispatch example:

export const mapDispatch = dispatch => ({
  postIntention: intention => dispatch(postIntention(intention))
})

export default connect(null, mapDispatch)(Form);

**Where we define the properties that will exsits in our global store

  1. npm i redux-thunk -S

Setup Backend

  1. Clone repo, not nested in project directory
  2. Globally install nodemon. Runs the server.
  3. npm install nodemon -g
  4. cd into repo
  5. Run npm install
  6. Run npm start
  7. Use Postman to checkout the data

Setting Up Testing

  1. Install Enzyme: npm i enzyme -D
  2. Install Enzyme Adapter: npm install enzyme-adapter-react-16 -D
  3. Inside of /src create setupTests.js: touch src/setupTests.js
  4. Put the following 3 lines of code in setupTests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });
  1. For snapshot testing, install Enzyme Wrappers: npm install enzyme-to-json --save-dev
  2. Add serializer to package.json, after devDependencies:
"jest": {
    "snapshotSerializers": [
      "enzyme-to-json/serializer"
    ],
    "coveragePathIgnorePatterns": [
      "src/index.js",
      "src/serviceWorker.js"
    ]
  }

Don't forget the comma!

  1. Get those sweet, sweet checkmarks in your tests - edit scripts line in package.json:
  "test": "react-scripts test --verbose"

Don't forget the comma!

  1. Add an extra line in App.js (just so there's a change in the file) and save. Then run npm test to check that the files are connected correctly. If you get an error about fsEvents not being a function, run the following commands in terminal:
npm audit fix --force

Run npm test to see if that worked. If not, run:

brew install watchman
  1. Include the following lines as headers for all test files:
import React from 'react';
import { shallow } from 'enzyme';
import ClassName from './ClassName';

Check Testing Coverage

npm test -- --coverage --watchAll=false

Setting Up ESLint

  1. ESLint is already built in with create-react-app. Installing another eslint will likely break things.
  2. Add a script called "lint": "eslint src/" in your package.json (in the scripts object)
  3. In your terminal, run: npm run lint
  4. Turing Mod3 Linter

Install SCSS/Sass

  1. Run: npm install node-sass --save
  2. Add variable file: touch src/variables.scss
  3. Change all .css file extentions to .scss
  • index.css App.css => index.scss App.scss
  1. Remember to update the file paths anywhere the style files are being imported
  2. Add: @import './src/variables.scss'; To any .scss files you want to uses variables
  3. Format for creating variables: $var-name: var-value;
  4. @mixins: touch src/mixins.scss
  5. create scss directory and move scss files (just variables and mixins) to the scss directory.
  6. Rounded corners:
@mixin rounded-corners($radius) {
  -webkit-border-radius: $radius;
     -moz-border-radius: $radius;
      -ms-border-radius: $radius;
          border-radius: $radius;
}

propTypes

npm install prop-types -S import PropTypes from 'prop-types'

Component.propTypes = {
  prop1: PropTypes.array.isRequired,
  prop2: PropTypes.any,
  prop3: PropTypes.func.isRequired,
  prop4: PropTypes.bool
  prop5: PropTypes.string.isRequired,
  prop6: PropTypes.number
}

Router

  1. npm i react-router-dom -S
  2. In index.js
import { BrowserRouter } from 'react-router-dom'

const router = (
  <BrowserRouter>
    <App />
  </BrowserRouter>
)

ReactDOM.render(router, document.getElementById('root'));
  1. Import what is needed in components import { Route, NavLink, Link, Redirect, Switch} from 'react-router-dom'

TESTING

Testing BoilerPlates

Snapshot Test

import React from 'react';
import { shallow } from 'enzyme';
import NameOfTested from './NameOfTested';

describe('NameOfTested', () => {
  it('should', () => {
    const wrapper = shallow(
      <Component />
    )
    expect(wrapper).toMatchSnapshot();
  })
})

Test Dynamic Changes Template

Setup - What do we need to do in order to render the component (aka shallow or mount). What data needs to be mocked?
Execution - Let’s run the command or simulate the action.
Expectation - This is where our assertion happens. After running our function, what did we expect to happen?

Update State Test

import React from 'react';
import ReactDOM from 'react-dom';
import { shallow } from 'enzyme';
import NameOfTested from './NameOfTested';

describe('NameOfTested', () => {
  it('should', () => {
    const wrapper = shallow(
      <Component />
    )
   const mockData = ?
   const expected = ?
   
   wrapper.instance().functionName(mockData);

   expect(wrapper.state('stateKeyName')).toEqual(expected);
  })
})

Function Test

it('should change state based on an event', () => {
    const mockFunction = jest.fn()
    const wrapper = shallow(<
      Component 
      submitUser={mockFunction}
    />)
    const mockEvent = {
      target: {
        name: 'name', value: 'value' 
      } 
    }

    wrapper.instance().handleChange(mockEvent)
    expect(wrapper.state('name')).toEqual('value')
  })

Simulate A Click Test

wrapper.instance().forceUpdate();

wrapper.find('element').simulate('click');
wrapper.find('element').at(0).simulate('click');
wrapper.find('button').simulate('click', mockEvent);

expect(mockFunction).toHaveBeenCalledWith(argument);
expect(wrapper.instance().resetInputs).toHaveBeenCalled();

Ex:
it('should run funcName on click', () => {
    const mockFuncName = jest.fn();
    const wrapper = shallow(
      <Component
        prop={look to parent to see what gets passed down}
        prop={look to parent to see what gets passed down}
      />
    )
    wrapper.find('element').at(0).simulate('click');

    expect(mockFuncName).toHaveBeenCalled();
  });
import { shallow } from 'enzyme';
wrapper = shallow (<Component prop='string' prop2={2} method={jest.fn()} />
expect(wrapper).toMatchSnapshot()

Execution

wrapper.instance().methodName('optionalArgument')
wrapper.instance().forceUpdate()
wrapper.find('button').simulate('click', optionalEvent)
wrapper.instance().setState({ prop: 'value', prop2: 'value2' })
wrapper.find('[name='thing']').simulate('change', mockThingEvent)
wrapper.find('button').at(3).simulate('click')
expect(mockWhatEverEvent).toHaveBeenCalledWith('argument/value')

Mock Date.now()

global.Date.now = jest.spyOn(global.Date, 'now').mockImplementation(() => 123)

Can assert that the value of Date.now() will be 123 or whatever is set as the return value.

global.Date.now = jest.fn().mockImplementation(() => 12345)

const expected = { title: '', description: '', id: 12345 };

Mock e.preventDefault() Can be passed as arugments of event in other methods

const mockEvent = { preventDefault: jest.fn() } 

Mock Event

const mockThingEvent = { target: { name: 'thing', value: 'Thing value' } }

Expectation

expect(wrapper.instance().handleChange).toHaveBeenCalledWith(mockThingEvent)
expect(wrapper.state('thing')).toEqual('Thing value') or expect(wrapper.state()).toEqual(expected)
expect(wrapper.instance().method).toHaveBeenCalled()

Network Requests

POST

const options = {
      method: 'POST',
      body: JSON.stringify({
        id: value,
        prop: value,
      }),
      headers: {
        'Content-Type': 'application/json'  
      }
    }
    
    return fetch('url', options)
            .then(res => {
              if(!res.ok) {
                throw Error('Something is not right, try again later')
              }
              return res.json()})

DELETE

const options = {
    method: 'DELETE',
    headers: {
      'Content-Type': 'application/json'
    }
  }

  return fetch(`url/${id}`, options)
    .then(res => {
      if(!res.ok) {
        throw Error('Something is not right, try again later')
      }
      return res.json()
    }).catch(error => {
      throw Error(error.message)
    });

handleChange

  handleChange = e => {
    this.setState({ [e.target.name]: e.target.value})
  }

testing async

import { getIdeas, postIdea } from './apiCalls'
// no need to import react
// no need to import enzyme
describe('apiCalls', () => {
  describe('getIdeas', () => {
    let mockResponse = [
      {
        id: 1,
        title: "Sweaters for pugs",
        description: "To keep them warm"
      }
    ]

    beforeEach(() => {
      window.fetch = jest.fn().mockImplementation(() => {
        return Promise.resolve({
          ok: true,
          json: () => {
            return Promise.resolve(mockResponse)
          }
        })
      })
    })

    it('should be passed the correct URL', () => {
      getIdeas()

      expect(window.fetch).toHaveBeenCalledWith('http://localhost:3001/api/v1/ideas')
    })
  
    it('should return an array of ideas', () => {
      expect(getIdeas()).resolves.toEqual(mockResponse)
    })
  
    it('should return an error for response that is !ok', () => {
      window.fetch = jest.fn().mockImplementation(() => {
        return Promise.resolve({
          ok: false
        })
      })
      expect(getIdeas()).rejects.toEqual(Error('Error fetching ideas'))
    })
  })

Boilerplates

Class Component

import React, { Component } from 'react';
import Classname from './Classname';
import Funcname from '../Funcname/Funcname';
import './App.css';

class App extends Component {
  constructor() {
    super();
    this.state = {
      key: value
    }
  }
  
  method = () => {
    console.log('run something')
  }
  
  render() {
    return (
      <div>
        Return something
      </div>
    )
  }
}

export default App;

Functional Component

import React from 'react';
import anotherFuncname from '../anotherFuncname/anotherFuncname';
import './Funcname.css';

const Funcname = () => {
  return (
    <div>
      Return something
    </div>
  )
}

export default Funcname;

Form

Functions needed

handleChange = event => {
    this.setState({
      [event.target.name]: event.target.value
    })
  }

  submitNewData = event => {
    event.preventDefault()
    const newData = {
      match key/value pairs of required data
    }
    this.props.funcFromApp(newData)
    this.clearInputs()
  }

  clearInputs = () => {
    this.setState({
      key1: '',
      key2: ''
    })
  }

Input Structure

<input
  name=''
  placeholder=''
  value={this.state.key}
  onChange={() => {}}
/>

setState

.then(ideas => this.setState({
        ideas: ideas
      }))
      
.then(ideas => this.setState({ ideas }))

this.setState({
    value: key
  })

setState Is Async

functionName = (parameter) => {
  this.setState({
    value: key
  }, () => {
    anotherFunctionName((parameter) => {
     return something
   }))
  });
 };

Getting Your Site on GitHub Pages

Preparation

  1. Checkout your master branch and double-check to make sure it is up-to-date and there is nothing needed to be committed at this point
  2. Navigate to the root of your project directory
  3. Open your project in your text editor
  4. Double-check that all image tags in your HTML have the src attribute have ./ to start the path, e.g src="./images/turing-logo.png"

Required Steps

  1. In the package.json file, within the "repository" object, edit the "url" value to be "git+https://github.com/USERNAME/REPONAME.git" where USERNAME and REPONAME are replaced with your GitHub username and your repository name, respectively

OR NOT?

  1. In the package.json file, add the line: "homepage": "http://USERNAME.github.io/REPONAME", where USERNAME and REPONAME are replaced with your GitHub username and your repository name, respectively

DO THIS "homepage": ".",

  1. Add these two lines to the scripts section of the package.json file:
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
  1. In the terminal, run npm install --save-dev gh-pages
  2. You should see these lines of JSON added to your package.json file:
"devDependencies": {
"gh-pages": "^1.1.0"
}
  1. Run npm run build in the command line
  2. Run npm run deploy in the command line

All of this will create a gh-pages branch in your repo with the contents from the build directory.

If you go to the GitHub pages site (http://USERNAME.github.io/REPONAME) in a minute, you should see your app! If not, check out the console to see what errors you're getting and troubleshoot from there.

Deploying with React Router

  1. npm install --save gh-pages
  2. “homepage”: “.“,
  3. “predeploy”: “npm run build”, “deploy”: “gh-pages -d build”
  4. import { HashRouter as Router } from ‘react-router-dom’ in index.js file and change to just <Router></Router>
  5. run npm run and build commands, done, go check GitHub Pages.

When You Make New Changes

If you make new changes to your master branch, GitHub Pages doesn't automatically know about these changes, and your site won't be up-to-date. You need to update the gh-pages branch to see those changes live on GitHub Pages. Here is how to update and keep everything in sync:

  1. After you're done making changes, checkout your master branch and double-check to make sure it is up-to-date and there is nothing needed to be committed at this point
  2. Run npm run build in the command line
  3. Run npm run deploy in the command line
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment