Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Sadhan Sarker
Sadhan Sarker

Posted on • Edited on

     

Journey for React & Redux, in a TDD Way

🔰 What is React?

React is a UI library built by Facebook. React gives us the ability to logically think about our frontend sites and apps.

🔰 What is Redux?

Redux is used mostly for application state management. Another way of looking at this - it helps you manage the data you display and how you respond to user actions.

Alt Text

Redux data flow (Image: Tanya Bachuk)

🔰 What is TDD?

Test-driven development (TDD), is an evolutionary approach to development which combines test-first development. where you write a test before you write just enough production code to fulfill that test and refactoring.

🔰 React Benefits are:

  • Strong community.
  • Can be made quickly.
  • Are easy to understand.
  • Clean & reuse programming.
  • Allow us to logically see the flow of data.
  • Scale well with small and large teams.
  • Transfer knowledge from desktop to mobile apps.

🔰 Recommended Knowledge (Prerequisites)

  • Knowledge of HTML & CSS.
  • Knowledge of JavaScript and ES6.
  • Some knowledge about the DOM.
  • Some knowledge about Node & npm.
  • Knowledge of basic Command line.

🔰 Knowledge of JavaScript and ES6

We need basic knowledge about ES6. Primarily 5 main syntax updates need to know, that are used heavily in React.

📗 1.let and const in addition to var:- Useconst orlet instead ofvar

varmessage='Hello! world';// ES5 Expressionletmessage='Hello! world';// ES6 Expressionconstmessage='Hello! world';// ES6 - const like constant or final
Enter fullscreen modeExit fullscreen mode

📗 2.Arrow Functions (=>):- is compact alternative to a regular function expression

// ES5 ExpressionfunctiongetGreetings(){return'Hello! From JavaScript.';};// ES5 ExpressionfunctionaddNumbers(a,b){returna+b;}// ES6 ExpressionconstgetGreetings=()=>{return'Hello! From JavaScript.';};// ES6 ExpressionconstaddNumbers=(a,b)=>a+b;// Or, ES6 ExpressionconstaddNumbers=(a,b)=>{returna+b;};
Enter fullscreen modeExit fullscreen mode

📗 3.Classes:-

JavaScript classes, introduced in ECMAScript 2015, are primarily syntactical sugar over JavaScript's existing prototype-based inheritance. The class syntax does not introduce a new object-oriented inheritance model to JavaScript.

classAppextendsReact.Component{render(){consttext="Hello! World";return(<div>Messageis:{text}</div>);}}
Enter fullscreen modeExit fullscreen mode

📗 4.Destructuring:-

Object destructuring and array destructuring are very easy ways to simplify our JavaScript code.

Object Destructing

// create an objectconstproduct={name:'Item 1',price:200};// we can access objectletname=product.name;letprice=product.price;// we can destructuring that object like belowlet{name,price}=product;// Module Import Issue,importReactDOMfrom'react-dom';// no destructuringimport{render}from'react-dom';// with destructuring
Enter fullscreen modeExit fullscreen mode

Array Destructuring

// create an arrayconstproduct=['item 1','item 2'];// access without destructuringletproduct1=product[0];letproduct2=product[1];// access with destructuringlet[p1,p2]=product;
Enter fullscreen modeExit fullscreen mode

📗 5.Spread:-

Spread syntax allows an iterable such as an array expression or string to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected, or an object expression to be expanded in places where zero or more key-value pairs (for object literals) are expected.

// Object spreadconstdefaults={name:'Product 1',price:200};constoptions={...defaults,visible:true};// Output: Object { name: "Product 1", price: 200, visible: true }//Array spreadconstroles=['admin','officer','executive'];constfullRoles=[...roles,'super-admin']//Output: Array ["admin", "officer", "executive", "super-admin"]
Enter fullscreen modeExit fullscreen mode

🔰 Create React App

We need to installyarn package manager alternate ofnpm so we can download packages Ultra Fast.
Based on operating system,Install it form here.

  • 4.Check environments is ready or not.
node--versionv10.15.3npm--version6.4.1yarn--version1.21.1
Enter fullscreen modeExit fullscreen mode

Note: Versions might be different, from me. Now we good to go forward

  • 4.Open terminal or command line and follow below commands,
# create new react app using commandnpx create-react-app tdd-react-redux# change directory or open directory using terminal commandcdtdd-react-redux# run projectnpm start
Enter fullscreen modeExit fullscreen mode
  • 5.Open project folder using VSCode, React offers us
tdd-react-redux├── public│   ├── favicon.ico│   ├── index.html│   └── manifest.json└── src    ├── App.css    ├── App.js    ├── App.test.js    ├── index.css    ├── index.js    ├── logo.svg    └── serviceWorker.js├── README.md├── node_modules├── package.json├── .gitignore
Enter fullscreen modeExit fullscreen mode
  • 6.Now install library, by opening terminal or command line
yarn add react-router-domyarn add node-sassyarn add prop-typesyarn add-D enzyme enzyme-adapter-react-16 jest jest-enzymeyarn add redux react-redux redux-thunkyarn add-D husky
Enter fullscreen modeExit fullscreen mode

🔰 Final project structure

tdd-react-redux└── src    ├── actions        ├── index.js        ├── types.js    ├── assets        ├── logo.png    ├── components        ├── core            ├── button            ├── headline            ├── listitem            ├── Common.js            ├── IconWithList.js            ├── PrintJson.js            ├── style.scss        ├── layouts        ├── About.js        ├── About.test.js        ├── BlogPost.js        ├── BlogPost.test.js        ├── DetailsPage.js        ├── DetailsPage.test.js        ├── style.scss    ├── reducers        ├── posts            ├── post.integration.test.js            ├── reducer.js        ├── index.js    ├── App.scss    ├── App.js    ├── App.test.js    ├── index.scss    ├── index.js    └── createStore.js    └── serviceWorker.js├── utils    ├── index.js├── .env├── .env.development├── .env.production├── .env.test├── README.md├── node_modules├── package.json├── .gitignore
Enter fullscreen modeExit fullscreen mode

Full Source Code

🔰 Let's come into the coding part

📗 Config helper Utilities.

1.Createutils/index.js file for test helper

importPropTypes,{checkPropTypes}from'prop-types';import{applyMiddleware,createStore}from'redux';importrootReducerfrom'./../src/reducers';import{middlewares}from'./../src/createStore';exportconstfindByTestAttr=(component,attr)=>{returncomponent.find(`[data-test='${attr}']`);};exportconstcheckProps=(component,expectedProps)=>{returncheckPropTypes(component.propTypes,expectedProps,'props',component.name)};exportconsttestStore=(initialState)=>{constcreateStoreWithMiddleware=applyMiddleware(...middlewares)(createStore);returncreateStoreWithMiddleware(rootReducer,initialState);};
Enter fullscreen modeExit fullscreen mode

3.Createsrc/createStore.js, to support redux and enable store,

import{createStore,applyMiddleware}from'redux';importReduxThunkfrom'redux-thunk';importRootReducerfrom'./reducers';exportconstmiddlewares=[ReduxThunk];exportconstcreateStoreWithMiddleware=applyMiddleware(...middlewares)(createStore);exportconststore=createStoreWithMiddleware(RootReducer);
Enter fullscreen modeExit fullscreen mode

4.UpdatesetupTests.js file, for enzyme support,

importEnzymefrom'enzyme';importEnzymeAdapterfrom'enzyme-adapter-react-16';Enzyme.configure({adapter:newEnzymeAdapter(),disableLifecycleMethods:true});
Enter fullscreen modeExit fullscreen mode

5.To Enable Redux Provider opensrc/index.js file and update it,

importReactfrom'react';importReactDOMfrom'react-dom';import{Provider}from'react-redux';import{store}from'./createStore';importAppfrom'./App';ReactDOM.render(<Providerstore={store}><App/></Provider>, document.getElementById('root'));
Enter fullscreen modeExit fullscreen mode

📗 6.Now, Config Redux Reducers

Create redux reducersrc/reducers/posts/reducer.spec.js test file

import{types}from'./../../actions/types';importpostReducerfrom'./reducer'describe('Posts Reducer',()=>{it('Should return default state',()=>{constnewState=postReducer(undefined,{});expect(newState).toEqual([]);});it('Should return new state if receiving type',()=>{constposts=[{title:'title 1',description:'description 1'},{title:'title 2',description:'description 2'}];constnewState=postReducer(undefined,{type:types.GET_POSTS,payload:posts});expect(newState).toEqual(posts);});});
Enter fullscreen modeExit fullscreen mode

Create redux reducersrc/reducers/posts/reducer.js

import{types}from'./../../actions/types';constpostReducer=(state=[],action)=>{switch(action.type){casetypes.GET_POSTS:returnaction.payload;casetypes.GET_FORTNITE_POSTS:returnaction.payload;default:returnstate;}};exportdefaultpostReducer;
Enter fullscreen modeExit fullscreen mode

Finally, create yoursrc/reducers/index.js and include

import{combineReducers}from'redux';importpostsfrom'./posts/reducer';exportdefaultcombineReducers({posts});
Enter fullscreen modeExit fullscreen mode

📗 6.Now Create Redux Actions

Createsrc/actions/types.js and include

exportconsttypes={GET_POSTS:'getPosts',GET_FORTNITE_POSTS:'getFortnite'};
Enter fullscreen modeExit fullscreen mode

Createsrc/actions/index.js and includes

import{types}from'./types';exportconstfetchPosts=()=>async(dispatch)=>{try{consturl="https://jsonplaceholder.typicode.com";constposts=awaitfetch(`${url}/posts?_limit=10`);constres=awaitposts.json();dispatch({type:types.GET_POSTS,payload:res});}catch(error){console.error("An error occurred");console.error(error);}};exportconstfetchFortnitePosts=()=>async(dispatch)=>{try{consturl="https://fortnite-api.theapinetwork.com/store/get";constresult=awaitfetch(url);constres=awaitresult.json();dispatch({type:types.GET_FORTNITE_POSTS,payload:res});}catch(error){console.error(error);}};
Enter fullscreen modeExit fullscreen mode

Finally, createsrc/reducers/posts/post.integration.spec.js integration test,

import{testStore}from'./../../../utils';import{fetchPosts,fetchFortnitePosts}from'./../../actions';describe('fetch api action',()=>{letstore;beforeEach(()=>{store=testStore();});it('Store is updated correctly',()=>{returnstore.dispatch(fetchPosts()).then(()=>{constnewState=store.getState();//console.log('response', newState.posts[0]);//expect(newState.posts[0].title).not(undefined);expect(newState.posts[0]).toHaveProperty('title');expect(newState.posts[0]).toHaveProperty('body');});});it('Store is update with Fortnite api data correctly',()=>{conststore=testStore();returnstore.dispatch(fetchFortnitePosts()).then(()=>{constnewState=store.getState();//console.log('output', newState.posts.data);expect(newState.posts.data[0]).toHaveProperty('itemId');expect(newState.posts.data[0].item).toHaveProperty('name');expect(newState.posts.data[0].item.images).toHaveProperty('icon');});});});
Enter fullscreen modeExit fullscreen mode

🔰 7.Setup React Component & Test Cases

📗 1. Nav Component,

createsrc/components/layouts/nav/index.spec.js

importReactfrom'react';import{shallow}from'enzyme';import{findByTestAttr,checkProps}from'../../../../utils'importNavfrom'./index';constsetUp=(props={})=>{returnshallow(<Nav/>);};describe('Index Component',()=>{letcomponent;beforeEach(()=>{component=setUp();});it('Should render without error',()=>{constwrapper=findByTestAttr(component,'navComponent');expect(wrapper.length).toBe(1)});});
Enter fullscreen modeExit fullscreen mode

createsrc/components/layouts/nav/index.js

importReactfrom'react';import'./style.scss';import{Link}from"react-router-dom";constNav=(props)=>{return(<divdata-test="navComponent"className="navComponent"><divclassName="leftMenus"><Linkto="/">Home</Link><Linkto="/about">About</Link></div><divclassName="rightMenus"><imgdata-test="userLogo"src="https://mesadhan.github.io/assets/profile_pic.jpg"alt="logo"/></div></div>)};exportdefaultNav;
Enter fullscreen modeExit fullscreen mode

📗 2. IconListItem Component

Createsrc/components/core/IconWithList.spec.js, and include

importReactfrom'react';import{shallow}from'enzyme';import{findByTestAttr,checkProps}from'../../../utils'importIconWithListfrom'./IconWithList';describe('IconWithList Component',()=>{describe('Checking PropTpes',()=>{it('Should not throwing warning',()=>{constexpectedProps={name:'item 1',icon:'icon',ratings:{}};constpropsError=checkProps(IconWithList,expectedProps);expect(propsError).toBeUndefined();});});describe('Should Renders',()=>{letcomponent;beforeEach(()=>{constprops={name:'item 1',icon:'icon',ratings:{avgStars:1,totalPoints:1,numberVotes:1},};component=shallow(<IconWithList{...props}/>);});it('Should render a ItemList',()=>{letitemList=findByTestAttr(component,'IconWithListComponent');expect(itemList.length).toBe(1);});it('Should render a name',()=>{lettitle=findByTestAttr(component,'componentTitle');expect(title.length).toBe(1);});it('Should render a Icon',()=>{lettitle=findByTestAttr(component,'componentIcon');expect(title.length).toBe(1);});it('Should render a Stars',()=>{lettitle=findByTestAttr(component,'componentStars');expect(title.length).toBe(1);});it('Should render a Points',()=>{lettitle=findByTestAttr(component,'componentPoints');expect(title.length).toBe(1);});it('Should render a Votes',()=>{lettitle=findByTestAttr(component,'componentVotes');expect(title.length).toBe(1);});});describe('Should Not Renders',()=>{letcomponent;beforeEach(()=>{component=shallow(<IconWithList/>);});it('Component is not render',()=>{letlistItem=findByTestAttr(component,'IconWithListComponent');expect(listItem.length).toBe(0);});});});
Enter fullscreen modeExit fullscreen mode

Createsrc/components/core/IconWithList.js, and include

importReact,{Component}from'react'importPropTypesfrom'prop-types';import'./style.scss'classIconWithListextendsComponent{render(){const{icon,name,ratings}=this.props;if(!name)returnnull;return(<divdata-test="IconWithListComponent"className="IconWithList"><imgdata-test="componentIcon"className="itemIcon"src={icon}alt="Icon"/><divclassName="itemBoxChildren"><h3className="itemTitle"data-test="componentTitle">{name}</h3><pdata-test="componentStars">Avg.Stars:-{ratings.avgStars}</p><pdata-test="componentPoints">TotalPoints:-{ratings.totalPoints}</p><pdata-test="componentVotes">Votes:-{ratings.numberVotes}</p></div></div>)}};IconWithList.propTypes={name:PropTypes.string};exportdefaultIconWithList;
Enter fullscreen modeExit fullscreen mode

📗 3. About Component

Createsrc/components/About.test.js, and include

importReactfrom'react';import{shallow}from'enzyme';import{findByTestAttr,checkProps}from'../../utils'importAboutfrom'./About';constsetUp=(props={})=>{returnshallow(<About/>);};describe('About Component',()=>{letcomponent;beforeEach(()=>{component=setUp();});it('Should render without error',()=>{constwrapper=findByTestAttr(component,'aboutComponent');expect(wrapper.length).toBe(1)});});
Enter fullscreen modeExit fullscreen mode

Createsrc/components/About.js, and include

importReact,{Component}from'react';classAboutextendsComponent{render(){document.title="About";return(<divdata-test="aboutComponent"><h1>HelloFromAboutPage</h1></div>);}}exportdefaultAbout;
Enter fullscreen modeExit fullscreen mode

📗 4. DetailsPage Component

Createsrc/components/DetailsPage.test.js, and include

importReactfrom'react';import{shallow}from'enzyme';import{findByTestAttr,checkProps}from'../../utils'importDetailsPagefrom'./DetailsPage';describe('DetailsPage Component',()=>{describe('Checking PropTpes',()=>{it('Should not throwing warning',()=>{constexpectedProps={name:'item 1',icon:'icon',ratings:{}};constpropsError=checkProps(DetailsPage,expectedProps);expect(propsError).toBeUndefined();});});describe('Should Renders',()=>{letcomponent;beforeEach(()=>{constprops={location:{state:{singlePost:{name:null,icon:null,ratings:{avgStars:1,totalPoints:1,numberVotes:1}}}}};component=shallow(<DetailsPage{...props}/>);});it('Component should render',()=>{letlistItem=findByTestAttr(component,'detailsPageComponent');expect(listItem.length).toBe(1);});it('Component should render name',()=>{letlistItem=findByTestAttr(component,'componentName');expect(listItem.length).toBe(1);});});});
Enter fullscreen modeExit fullscreen mode

Createsrc/components/DetailsPage.js, and include

importReact,{Component}from'react';import'./style.scss'importPropTypesfrom"prop-types";importIconWithListfrom"./core/IconWithList";classDetailsPageextendsComponent{constructor(props){super(props);document.title="Details Page";}render(){const{name,icon,ratings}=this.props.location.state.singlePost;return(<divdata-test="detailsPageComponent"className="DetailsPageComponent"><h1>ProductDetails</h1><divclassName="DetailsBox"><div><imgdata-test="componentIcon"className="iconBox"src={icon}/></div><divclassName="itemDetails"><h3data-test="componentName">{name}</h3><p>Avg.Stars:{ratings.avgStars}</p><p>TotalPoints:{ratings.totalPoints}</p><p>Votes:{ratings.numberVotes}</p></div></div></div>);}}DetailsPage.propTypes={name:PropTypes.string};exportdefaultDetailsPage;
Enter fullscreen modeExit fullscreen mode

📗 5. App Component

Createsrc/App.test.js, and include

importReactfrom'react';import{shallow}from'enzyme';import{findByTestAttr,checkProps}from'./../utils'importAppfrom'./App';constsetUp=(props={})=>{returnshallow(<App/>);};describe('App Component',()=>{letcomponent;beforeEach(()=>{component=setUp();});it('Should render without error',()=>{constwrapper=findByTestAttr(component,'appComponent');expect(wrapper.length).toBe(1)});});
Enter fullscreen modeExit fullscreen mode

Createsrc/App.js, and include

importReact,{Component}from'react';importNavfrom'./components/layouts/nav';importHomefrom'./components/Home';importAboutfrom'./components/About';import'./app.scss'import{BrowserRouterasRouter,Switch,Route,}from"react-router-dom";importBlogPostfrom"./components/BlogPost";importDetailsPagefrom"./components/DetailsPage";classAppextendsComponent{render(){return(<Routerbasename={process.env.PUBLIC_URL}><divdata-test="appComponent"className="App"><Nav/><Switch><Routepath="/"exactcomponent={Home}/><Routepath="/item/:id"exactcomponent={DetailsPage}/><Routepath="/about"exactcomponent={About}/></Switch></div></Router>);}}exportdefaultApp;
Enter fullscreen modeExit fullscreen mode

📗 6. Home Component Setup

Createsrc/components/Home.test.js

importReactfrom'react';import{shallow}from'enzyme';import{findByTestAttr,testStore}from'../../utils';importHomefrom"./Home";constsetUp=(initialState={})=>{conststore=testStore(initialState);constcomponent=shallow(<Homestore={store}/>).childAt(0).dive();//console.log( component.debug() );returncomponent;};describe('Home Component',()=>{letcomponent;beforeEach(()=>{constinitialState={posts:[{title:'title 1',body:'Body 1'},{title:'title 2',body:'Body 2'},{title:'title 3',body:'Body 3'}]};component=setUp(initialState)});it('Should render without errors',()=>{letc=findByTestAttr(component,'homeComponent');expect(c.length).toBe(1);});});
Enter fullscreen modeExit fullscreen mode

Createsrc/components/Home.js

importReact,{Component}from'react';importIconWithListfromfrom'./core/IconWithList'import{connect}from'react-redux';import{fetchFortnitePosts,fetchPosts}from'../actions';import'./style.scss'import{Link}from"react-router-dom";classHomeextendsComponent{constructor(props){super(props);this.loadData=this.loadData.bind(this);this.loadData();// initially load data}loadData(){this.props.fetchFortnitePosts();}render(){const{dumPosts,fortnitePosts}=this.props;document.title="Welcome";return(<divdata-test="homeComponent"className="Home"><sectionclassName="main">{fortnitePosts&&<div>{fortnitePosts.map((data,index)=>{const{itemId}=data;constconfigurationListItem={name:data.item.name,icon:data.item.images.icon,ratings:data.item.ratings};return(<Linkto={{pathname:`/item/${itemId}`,state:{singlePost:configurationListItem}}}style={{textDecoration:'none'}}key={index}><IconWithListfrom{...configurationListItem}/></Link>)})}</div>}</section></div>);}}constmapStateToProps=(state)=>{return{dumPosts:state.posts,fortnitePosts:state.posts.data}};// if we and to override dispatcher methodconstmapDispatchToProps=dispatch=>({fetchPosts:()=>dispatch(fetchPosts()),fetchFortnitePosts:()=>dispatch(fetchFortnitePosts()),});//export default connect(mapStateToProps, { fetchPosts })(Home);exportdefaultconnect(mapStateToProps,mapDispatchToProps)(Home);
Enter fullscreen modeExit fullscreen mode

🔰 Environment Variable Setup

Note: The prefixREACT_APP_ is required when creating custom environment variables.

.env,.env.development,.env.test and.env.production
As a default behavior, those files will be served with no configuration. You do not even have to update scripts in package.json

.env.staging
Here is the main focus. To target.env.staging file for the staging build, we need a library to achieve this.

  • 1.Let's install env-cmd. This library will will help us on using/executing a selected environment file.See more detail
// executecommandbelow at the root of projectnpminstallenv-cmd--saveOr,yarn add env-cmd
Enter fullscreen modeExit fullscreen mode
  • 2.Add a script in package.json like below.
// package.jsonscripts:{"start":"react-scripts start",// `NODE_ENV` is equal to `development`."build":"react-scripts build",// `NODE_ENV` is equal to `production`."build:staging":"env-cmd -f .env.staging react-scripts build",// `NODE_ENV` is equal to `production`....}
Enter fullscreen modeExit fullscreen mode
  • 3.Finally, test yourbuild:staging` script.

🔰 Husky Configuration

Before push into git, we like to pass our test successfully. We already install dependency now just need to configure it,

Openpackage.json and update with

"scripts": {    //... more  },  "husky": {    "hooks": {      "pre-push": "CI=true npm test"    }  }
Enter fullscreen modeExit fullscreen mode

🔰 To run Application

# For run test suiteyarn run test# Run applicationyarn start
Enter fullscreen modeExit fullscreen mode

👌 Congratulations. It's a long tutorial!. & Thanks for your time & passion.
Feel free to comments, If you have any issues & queries.

🔰 References

Top comments(2)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
kimsean profile image
thedevkim
Software Engineer 👨‍💻 Coffee ☕ Chocolates🍫
  • Location
    Philippines
  • Work
    Software Engineer
  • Joined

best article that i've been looking for. Thank you so much!

CollapseExpand
 
mesadhan profile image
Sadhan Sarker
I’m very much eager about new tools and technologies. I love to work with the R&D Team. Till now my findings, I believe nothing is impossible just need focus, dedication, and time.
  • Location
    Dhaka, Bangladesh
  • Work
    Full-Stack Software Engineer
  • Joined

Very welcome! If you benefited from it. Very encouraging comments for me.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

I’m very much eager about new tools and technologies. I love to work with the R&D Team. Till now my findings, I believe nothing is impossible just need focus, dedication, and time.
  • Location
    Dhaka, Bangladesh
  • Work
    Full-Stack Software Engineer
  • Joined

More fromSadhan Sarker

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp