├── 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 | {
16 | dispatch(addMessage(input.value))
17 | input.value = ''
18 | }}>
19 | SEND
20 |
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 |
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 |
26 | You need to enable JavaScript to run this app.
27 |
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 |
--------------------------------------------------------------------------------