├── src ├── index.css ├── reducers │ ├── index.js │ └── messages.js ├── App.test.js ├── components │ ├── App.js │ ├── MessageList.js │ ├── MessageBar.js │ └── MessageItemRow.js ├── actions │ └── index.js ├── App.css ├── index.js └── registerServiceWorker.js ├── public ├── favicon.ico ├── manifest.json └── index.html ├── .gitignore ├── package.json └── README.md /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steam/react-redux-walkthrough/master/public/favicon.ico -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import messages from './messages' 3 | 4 | const app = combineReducers({ 5 | messages 6 | }) 7 | 8 | export default app 9 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import MessageBar from './MessageBar' 3 | import MessageList from './MessageList' 4 | import '../App.css' 5 | 6 | const App = () => ( 7 |
8 | 9 | 10 |
11 | ) 12 | 13 | export default App 14 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid' 2 | 3 | export const addMessage = text => ({ 4 | type: 'ADD_MESSAGE', 5 | name: "Sean", 6 | key: v4(), 7 | id: v4(), 8 | time: new Date().toString(), 9 | avatar: "https://staging.cirrusmd.com/images/default-profile.svg", 10 | text 11 | }) 12 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /src/reducers/messages.js: -------------------------------------------------------------------------------- 1 | const messages = (state = [], action) => { 2 | switch (action.type) { 3 | case 'ADD_MESSAGE': 4 | return [ 5 | ...state, 6 | { 7 | id: action.id, 8 | key: action.key, 9 | text: action.text, 10 | name: action.name, 11 | time: action.time, 12 | avatar: action.avatar 13 | } 14 | ] 15 | default: 16 | return state 17 | } 18 | } 19 | 20 | export default messages 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "lodash": "^4.17.4", 7 | "react": "^15.6.1", 8 | "react-dom": "^15.6.1", 9 | "react-redux": "^5.0.6", 10 | "react-scripts": "1.0.10", 11 | "redux": "^3.7.2", 12 | "uuid": "^3.1.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/MessageList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import MessageItemRow from './MessageItemRow' 4 | 5 | let MessageList = ({messages}) => { 6 | const rows = messages.map((message) => { 7 | return( 8 | 12 | ) 13 | }) 14 | return (
{rows}
) 15 | } 16 | 17 | const mapStateToProps = state => ({ 18 | messages: state.messages 19 | }) 20 | 21 | MessageList = connect( 22 | mapStateToProps 23 | )(MessageList) 24 | 25 | export default MessageList 26 | -------------------------------------------------------------------------------- /src/components/MessageBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { addMessage } from '../actions' 4 | 5 | let MessageBar = ({dispatch}) => { 6 | let input 7 | return ( 8 |
9 | { 12 | input = node 13 | }} 14 | /> 15 | 21 |
22 | ) 23 | } 24 | 25 | MessageBar = connect()(MessageBar) 26 | 27 | export default MessageBar 28 | -------------------------------------------------------------------------------- /src/components/MessageItemRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const MessageItemRow = ({message}) => { 4 | const time = new Date(message.time) 5 | return ( 6 |
7 | avatar 12 | 13 | {message.name}  14 | {time.getHours() + ":" + time.getMinutes()} 15 | 16 |
17 |
18 | {message.text} 19 |
20 |
21 |
22 | ) 23 | } 24 | 25 | export default MessageItemRow 26 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 5px 3 | } 4 | 5 | #container { 6 | font-family: "Roboto", sans-serif; 7 | } 8 | 9 | img.avatar { 10 | display: inline-block; 11 | float: left; 12 | width: 40px; 13 | height: 40px; 14 | margin-right: 20px; 15 | vertical-align: top; 16 | border-radius: 100%; 17 | border-style: solid; 18 | border-width: 1px; 19 | border-color: transparent; 20 | box-shadow: 0 0 0 2px #CCCCCC; 21 | } 22 | 23 | span.name { 24 | display: inline-block; 25 | white-space: nowrap; 26 | font-weight: 900; 27 | font-size: 11px; 28 | } 29 | 30 | span.name em { 31 | color: #a6a6a6; 32 | } 33 | 34 | .message-text { 35 | color: #4D4D4D; 36 | font-size: 16px; 37 | line-height: 19px; 38 | font-weight: 300; 39 | } 40 | 41 | .message { 42 | position: relative; 43 | clear: both; 44 | margin: 0 0 30px; 45 | white-space: nowrap; 46 | padding-left: 2px; 47 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import App from './components/App' 4 | import registerServiceWorker from './registerServiceWorker' 5 | import { Provider } from 'react-redux' 6 | import { createStore } from 'redux' 7 | import app from './reducers' 8 | 9 | const initialMessages = [ 10 | { 11 | text: "Hi, I'm worried I have something wrong with my ankle.", 12 | name: "Sean", 13 | key: "unique1", 14 | id: "unique1", 15 | time: new Date(), 16 | avatar: "https://staging.cirrusmd.com/images/default-profile.svg" 17 | }, 18 | { 19 | text: "I twisted it while walking down the street.", 20 | name: "Sean", 21 | key: "unique2", 22 | id: "unique2", 23 | time: new Date(), 24 | avatar: "https://staging.cirrusmd.com/images/default-profile.svg" 25 | } 26 | ] 27 | 28 | let store = createStore(app, { messages: initialMessages }) 29 | 30 | render( 31 | 32 | 33 | , 34 | document.getElementById('root') 35 | ) 36 | 37 | registerServiceWorker() 38 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + Redux 2 | 3 | ### **React** - A Javascript library for building user interfaces 4 | 5 | [https://facebook.github.io/react/](https://facebook.github.io/react/) 6 | 7 | >React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes. 8 | 9 | ### Getting Started with React 10 | 1. `brew install yarn` 11 | 2. `yarn global add create-react-app` 12 | 3. `create-react-app my-app` 13 | 4. `cd my-app && yarn start` 14 | 15 | #### JSX - JSX is a preprocessor step that adds XML syntax to JavaScript. 16 | 17 | [http://buildwithreact.com/tutorial/jsx](http://buildwithreact.com/tutorial/jsx) 18 | 19 | You can definitely use React without JSX but JSX makes React a lot more elegant. 20 | 21 | ``` 22 |
Hello
23 | ``` 24 | 25 | #### Class Component - can have internal state 26 | ``` 27 | class HelloMessage extends React.Component { 28 | render() { 29 | return
Hello
30 | } 31 | } 32 | ``` 33 | 34 | 35 | #### Pure Functional Component - has no internal state 36 | ``` 37 | const HelloMessage = () =>
Hello
38 | ``` 39 | 40 | 41 | #### Props - component input data (accessed via this.props) 42 | 43 | ``` 44 | const HelloMessage = (props) =>
{this.props.name}
45 | 46 | ReactDOM.render(, mountNode); 47 | ``` 48 | 49 | #### State - component internal state data (accessed via this.state) 50 | 51 | ``` 52 | class Timer extends React.Component { 53 | constructor(props) { 54 | super(props); 55 | this.state = {secondsElapsed: 0}; 56 | } 57 | 58 | tick() { 59 | this.setState((prevState) => ({ 60 | secondsElapsed: prevState.secondsElapsed + 1 61 | })); 62 | } 63 | 64 | componentDidMount() { 65 | this.interval = setInterval(() => this.tick(), 1000); 66 | } 67 | 68 | componentWillUnmount() { 69 | clearInterval(this.interval); 70 | } 71 | 72 | render() { 73 | return ( 74 |
Seconds Elapsed: {this.state.secondsElapsed}
75 | ); 76 | } 77 | } 78 | 79 | ReactDOM.render(, mountNode); 80 | ``` 81 | 82 | --- 83 | 84 | ### **Redux** - A predictable state container for JavaScript apps 85 | 86 | [http://redux.js.org/](http://redux.js.org/) 87 | 88 | >The whole state of your app is stored in an object tree inside a single store. 89 | >The only way to change the state tree is to emit an action, an object describing what happened. 90 | >To specify how the actions transform the state tree, you write pure reducers. 91 | 92 | >That's it! 93 | 94 | Redux architecture revolves around a strict unidirectional data flow. 95 | 96 | This means that all data in an application follows the same lifecycle pattern, making the logic of your app more predictable and easier to understand. It also encourages data normalization, so that you don't end up with multiple, independent copies of the same data that are unaware of one another. 97 | 98 | 1. You call store.dispatch(action). 99 | 2. The Redux store calls the reducer function you gave it. 100 | 3. The root reducer may combine the output of multiple reducers into a single state tree. 101 | 4. The Redux store saves the complete state tree returned by the root reducer. 102 | 103 | ### Getting Started with React-Redux 104 | 1. `yarn add redux` 105 | 2. `yarn add react-redux` 106 | 3. `yarn add uuid` 107 | 108 | --- 109 | 110 | ## Learning Resources 111 | 112 | [Start Using React to Build Web Applications](https://egghead.io/courses/start-using-react-to-build-web-applications) - Getting started with React video series. 113 | 114 | [Getting Started with Redux](https://egghead.io/courses/getting-started-with-redux) - Excellent video series by the created of Redux, Dan Abramov 115 | 116 | [Building React Applications with Idiomatic Redux](https://egghead.io/courses/building-react-applications-with-idiomatic-redux) - Another, more in depth video series by Dan Abramov 117 | 118 | [React, Redux and react-redux](http://jilles.me/react-redux-and-react-redux/) - building a small react app, adding redux, then adding react-redux 119 | 120 | 121 | 122 | ## Further Reading 123 | 124 | **Immutable JS** - Immutable collections for JavaScript 125 | 126 | [https://facebook.github.io/immutable-js/](https://facebook.github.io/immutable-js/) 127 | 128 | **React Router** - Declarative routing for React 129 | 130 | [https://github.com/ReactTraining/react-router](https://github.com/ReactTraining/react-router) 131 | 132 | **Reselect** - Selector library for Redux 133 | 134 | [https://github.com/reactjs/reselect](https://github.com/reactjs/reselect) 135 | 136 | **Redux Saga** - A library that aims to make side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) in React/Redux applications easier and better. 137 | 138 | [https://redux-saga.js.org/](https://redux-saga.js.org/) 139 | 140 | **Typescript** - A typed superset of Javascript that compiles to plain Javascript. 141 | 142 | [https://www.typescriptlang.org/](https://www.typescriptlang.org/) 143 | --------------------------------------------------------------------------------