├── docs
├── README.md
├── Gemfile
├── index.md
├── _sass
│ ├── _tiles.scss
│ └── type-on-strap.scss
├── _config.yml
├── _layouts
│ ├── page.html
│ └── home.html
├── assets
│ ├── redux-logo.svg
│ └── react-logo.svg
├── _includes
│ └── toc.html
├── pages
│ ├── redux-ramda.md
│ └── react-ramda.md
└── Gemfile.lock
├── src
├── Ex01
│ ├── index.js
│ ├── README.md
│ ├── Loading.js
│ └── Ex01.js
├── Ex02
│ ├── index.js
│ ├── README.md
│ ├── Content.js
│ └── Ex02.js
├── Ex03
│ ├── index.js
│ ├── README.md
│ ├── Content.js
│ └── Ex03.js
├── Ex04
│ ├── index.js
│ ├── README.md
│ ├── Ex04.js
│ └── Section.js
├── index.css
└── index.js
├── .eslintrc.json
├── public
├── favicon.ico
├── manifest.json
└── index.html
├── README.md
├── .editorconfig
├── .gitignore
└── package.json
/docs/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Ex01/index.js:
--------------------------------------------------------------------------------
1 | export * from './Ex01';
--------------------------------------------------------------------------------
/src/Ex02/index.js:
--------------------------------------------------------------------------------
1 | export * from './Ex02';
--------------------------------------------------------------------------------
/src/Ex03/index.js:
--------------------------------------------------------------------------------
1 | export * from './Ex03';
--------------------------------------------------------------------------------
/src/Ex04/index.js:
--------------------------------------------------------------------------------
1 | export * from './Ex04';
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | gem "github-pages", group: :jekyll_plugins
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 | hide: true
4 | ---
5 |
6 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": "eslint-config-react-app"
4 | }
--------------------------------------------------------------------------------
/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/tommmyy/ramda-react-redux-patterns/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/Ex01/README.md:
--------------------------------------------------------------------------------
1 | # Example
2 |
3 | In this example we show how to use `R.always` to generate static component.
--------------------------------------------------------------------------------
/src/Ex03/README.md:
--------------------------------------------------------------------------------
1 | # Example
2 |
3 | In this example we show how `R.cond` can be used as HoC for conditional render.
--------------------------------------------------------------------------------
/src/Ex02/README.md:
--------------------------------------------------------------------------------
1 | # Example
2 |
3 | In this example we show how `R.ifElse` can be used as HoC for conditional render.
--------------------------------------------------------------------------------
/src/Ex04/README.md:
--------------------------------------------------------------------------------
1 | # Example
2 |
3 | In this example we show how `R.pick` can be used as HoC for filtering the valid properties.
--------------------------------------------------------------------------------
/src/Ex01/Loading.js:
--------------------------------------------------------------------------------
1 | import { always } from 'ramda';
2 |
3 | const Loading = always("Loading...");
4 |
5 | export { Loading };
6 |
--------------------------------------------------------------------------------
/src/Ex01/Ex01.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Loading } from './Loading';
3 |
4 | const Ex01 = () => ;
5 |
6 | export { Ex01 };
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ramda: patterns for React and Redux
2 |
3 | [➡️ tommmyy.github.io/ramda-react-redux-patterns ⬅️](https://tommmyy.github.io/ramda-react-redux-patterns/)
4 |
5 | **⚠️ Work in progress ⚠️**
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import { Ex02 } from './Ex02';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 |
6 | [*.md]
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
--------------------------------------------------------------------------------
/docs/_sass/_tiles.scss:
--------------------------------------------------------------------------------
1 | .tiles {
2 | display: flex;
3 | text-align: center;
4 |
5 | &-tile {
6 | width: 50%;
7 | display: inline-block;
8 | }
9 |
10 | .img {
11 | width: 100%;
12 | height: auto;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Ex04/Ex04.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Section } from './Section';
3 |
4 | class Ex04 extends Component {
5 | render() {
6 | return (
7 |
10 | );
11 | }
12 | }
13 |
14 | export { Ex04 };
15 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
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 |
23 | .sass-cache
24 | NOTES.md
25 | /docs/_site
--------------------------------------------------------------------------------
/src/Ex02/Content.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { always, prop, ifElse } from 'ramda';
3 |
4 | const Loading = always("Loading...");
5 | const Section = ({ content }) => ;
6 |
7 | // Component -> Component
8 | const withLoading = ifElse(prop('loading'), Loading)
9 |
10 | // const Content = (props) => props.loading ? :
11 | const Content = withLoading(Section)
12 |
13 | export { Content };
14 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | #baseurl: ""
2 | #url: ""
3 |
4 | remote_theme: Sylhare/Type-on-Strap
5 |
6 | theme_settings:
7 | title: Use Ramda with React and Redux
8 | avatar: assets/img/triangle.svg
9 | description: Patterns and techniques for Ramda and React with Ramda library
10 | github: "tommmyy"
11 | google_fonts: Source+Sans+Pro:400,700,700italic,400italic
12 | footer_text: 2018
13 |
14 | title: React and Ramda patterns
15 | kramdown:
16 | parse_block_html: true
17 |
--------------------------------------------------------------------------------
/src/Ex03/Content.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { T, always, prop, cond, isEmpty, map, o } from 'ramda';
3 |
4 | const Loading = always("Loading...");
5 | const Missing = always("No results.");
6 |
7 | const Section = ({ items, children }) => ;
8 |
9 | // Component -> Component
10 | const Content = cond([
11 | [prop('loading'), Loading],
12 | [o(isEmpty, prop('items')), Missing],
13 | [T, Section],
14 | ]);
15 |
16 | export { Content };
17 |
--------------------------------------------------------------------------------
/src/Ex02/Ex02.js:
--------------------------------------------------------------------------------
1 | import { complement } from 'ramda';
2 | import React, { Component } from 'react';
3 | import { Content } from './Content';
4 |
5 | class Ex02 extends Component {
6 | state = { content: null };
7 |
8 | componentDidMount() {
9 | setInterval(() => this.setState({ content: "Loaded content." }), 2000)
10 | }
11 |
12 | render() {
13 | const { content } = this.state;
14 |
15 | return ;
16 | }
17 | }
18 |
19 | export { Ex02 };
20 |
--------------------------------------------------------------------------------
/docs/_sass/type-on-strap.scss:
--------------------------------------------------------------------------------
1 | @import 'base/variables';
2 |
3 | // External
4 | @import 'external/reset';
5 | @import 'external/syntax';
6 |
7 | // Base
8 | @import 'base/global';
9 | @import 'base/utility';
10 |
11 | // Posts
12 | @import 'layouts/posts';
13 | @import 'layouts/index';
14 | @import 'layouts/page';
15 | @import 'layouts/tags';
16 | @import 'layouts/search';
17 | @import 'layouts/portfolio';
18 |
19 | // Includes
20 | @import 'includes/footer';
21 | @import 'includes/post_nav';
22 | @import 'includes/navbar';
23 |
24 | @import 'tiles';
25 |
26 |
--------------------------------------------------------------------------------
/src/Ex03/Ex03.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Content } from './Content';
3 |
4 | const renderItem = (x) =>
{x}!
;
5 |
6 | class Ex03 extends Component {
7 | state = { loading: true, items: null };
8 |
9 | componentDidMount() {
10 | setInterval(() => {
11 | // this.setState({ items: [], loading: false });
12 | this.setState({ items:["Pichu", "Pikachu", "Raichu"], loading: false })
13 | }, 2000);
14 | }
15 |
16 | render() {
17 | const { items, loading } = this.state;
18 |
19 | return {renderItem};
20 | }
21 | }
22 |
23 | export { Ex03 };
24 |
--------------------------------------------------------------------------------
/docs/_layouts/page.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 |
7 |
8 | {{ page.title }}
9 |
10 |
11 | {% if page.subtitle %}
12 | {{ page.subtitle }}
13 | {% endif %}
14 |
15 |
16 | {% include toc.html html=content h_min=2 %}
17 |
18 |
19 |
20 |
21 |
22 | {% capture tag_list %}{{ page.tags | join: "|"}}{% endcapture %}
23 | {% include tags_list.html tags=tag_list %}
24 |
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ramda-react-redux-patterns",
3 | "version": "1.0.0",
4 | "description": "Patterns for Ramda, Redux with Ramda",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/tommmyy/react-redux-ramda-patterns.git"
8 | },
9 | "keywords": [
10 | "javascript",
11 | "guide",
12 | "programming patterns",
13 | "functional programming",
14 | "ramda",
15 | "react",
16 | "redux"
17 | ],
18 | "author": "Tomas Konrady ",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/tommmyy/react-redux-ramda-patterns/issues"
22 | },
23 | "private": true,
24 | "dependencies": {
25 | "ramda": "^0.25.0",
26 | "ramda-extension": "^0.3.5",
27 | "react": "^16.3.1",
28 | "react-dom": "^16.3.1",
29 | "react-scripts": "1.1.4"
30 | },
31 | "scripts": {
32 | "start": "react-scripts start",
33 | "build": "react-scripts build",
34 | "test": "react-scripts test --env=jsdom",
35 | "eject": "react-scripts eject"
36 | }
37 | }
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/Ex04/Section.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import * as R from 'ramda';
4 |
5 | const SimpleSection = ({ heading, childrenLength, children, ...rest }) => (
6 |
7 | {heading}
8 | {children}
9 | {childrenLength}
10 |
11 | );
12 |
13 | SimpleSection.propTypes = {
14 | heading: PropTypes.string,
15 | children: PropTypes.string,
16 | childrenLength: PropTypes.number,
17 | className: PropTypes.string,
18 | };
19 |
20 | const pickByPropTypes = R.useWith(R.pick, [R.o(R.keys, R.prop('propTypes'))]);
21 |
22 | //const withValidProps_ = (C) => (props) =>
23 |
24 | // Props -> Component -> Element
25 | const renderComponent = R.curryN(2, React.createElement);
26 |
27 | // const withValidProps = (C) => (ps) => renderComponent(C, pickByPropTypes(C)(ps))
28 | // const withValidProps = (C) => (ps) => renderComponent(C, pickByPropTypes(C)(ps))
29 | //const Section = withValidProps(SimpleSection);
30 |
31 | // mapProps :: (a -> a) -> Component -> Component
32 | // const mapProps = R.curry((mapping, C) => (props) => renderComponent(C)(mapping(props)))
33 | // const mapProps = R.curry((mapping, C) => R.o(renderComponent(C), mapping))
34 | const mapProps = R.flip(R.useWith(R.o, [renderComponent, R.identity]));
35 |
36 | const addLength = ({ children }) => ({ childrenLengthgth: R.length(children) })
37 |
38 | const computeProps = R.converge(R.merge, [R.nthArg(1), R.call]);
39 |
40 | const setDisplayName = (displayName) => R.tap((C) => C.displayName = displayName)
41 |
42 | const Section = R.compose(
43 | setDisplayName("Section"),
44 | mapProps(
45 | R.compose(
46 | R.evolve({ heading: R.toUpper }),
47 | computeProps(addLength),
48 | pickByPropTypes(SimpleSection)
49 | )
50 | )
51 | )(SimpleSection);
52 |
53 |
54 | export { Section };
--------------------------------------------------------------------------------
/docs/assets/redux-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/docs/_layouts/home.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 | {% if site.theme_settings.header_text %}
7 |
9 | {{ site.theme_settings.header_text }}
10 |
11 | {% endif %}
12 |
13 |
14 |
24 |
25 |
26 |
27 |
28 |
29 | {% for post in paginator.posts %}
30 |
31 | {% if post.thumbnail %}
32 |
33 |

34 |
35 | {% endif %}
36 |
37 |
47 |
48 | {{ post.excerpt | strip_html }}
49 |
50 |
53 |
54 |
55 |
56 | {% endfor %}
57 |
58 |
59 | {% if paginator.total_pages > 1 %}
60 |
74 | {% endif %}
75 |
--------------------------------------------------------------------------------
/docs/assets/react-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------
/docs/_includes/toc.html:
--------------------------------------------------------------------------------
1 | {% capture tocWorkspace %}
2 | {% comment %}
3 | Version 1.0.4
4 | https://github.com/allejo/jekyll-toc
5 |
6 | "...like all things liquid - where there's a will, and ~36 hours to spare, there's usually a/some way" ~jaybe
7 |
8 | Usage:
9 | {% include toc.html html=content sanitize=true class="inline_toc" id="my_toc" h_min=2 h_max=3 %}
10 |
11 | Parameters:
12 | * html (string) - the HTML of compiled markdown generated by kramdown in Jekyll
13 |
14 | Optional Parameters:
15 | * sanitize (bool) : false - when set to true, the headers will be stripped of any HTML in the TOC
16 | * class (string) : '' - a CSS class assigned to the TOC
17 | * id (string) : '' - an ID to assigned to the TOC
18 | * h_min (int) : 1 - the minimum TOC header level to use; any header lower than this value will be ignored
19 | * h_max (int) : 6 - the maximum TOC header level to use; any header greater than this value will be ignored
20 | * ordered (bool) : false - when set to true, an ordered list will be outputted instead of an unordered list
21 | * item_class (string) : '' - add custom class for each list item; has support for '%level%' placeholder, which is the current heading level
22 |
23 | Output:
24 | An ordered or unordered list representing the table of contents of a markdown block. This snippet will only generate the table of contents and will NOT output the markdown given to it
25 | {% endcomment %}
26 |
27 | {% capture my_toc %}{% endcapture %}
28 | {% assign orderedList = include.ordered | default: false %}
29 | {% assign minHeader = include.h_min | default: 1 %}
30 | {% assign maxHeader = include.h_max | default: 6 %}
31 | {% assign nodes = include.html | split: ' maxHeader %}
44 | {% continue %}
45 | {% endif %}
46 |
47 | {% if firstHeader %}
48 | {% assign firstHeader = false %}
49 | {% assign minHeader = headerLevel %}
50 | {% endif %}
51 |
52 | {% assign indentAmount = headerLevel | minus: minHeader | add: 1 %}
53 | {% assign _workspace = node | split: '' | first }}>{% endcapture %}
60 | {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %}
61 |
62 | {% assign space = '' %}
63 | {% for i in (1..indentAmount) %}
64 | {% assign space = space | prepend: ' ' %}
65 | {% endfor %}
66 |
67 | {% unless include.item_class == blank %}
68 | {% capture listItemClass %}{:.{{ include.item_class | replace: '%level%', headerLevel }}}{% endcapture %}
69 | {% endunless %}
70 |
71 | {% capture my_toc %}{{ my_toc }}
72 | {{ space }}{{ listModifier }} {{ listItemClass }} [{% if include.sanitize %}{{ header | strip_html }}{% else %}{{ header }}{% endif %}](#{{ html_id }}){% endcapture %}
73 |
74 | {% endfor %}
75 |
76 | {% if include.class %}
77 | {% capture my_toc %}{:.{{ include.class }}}
78 | {{ my_toc | lstrip }}{% endcapture %}
79 | {% endif %}
80 |
81 | {% if include.id %}
82 | {% capture my_toc %}{: #{{ include.id }}}
83 | {{ my_toc | lstrip }}{% endcapture %}
84 | {% endif %}
85 | {% endcapture %}{% assign tocWorkspace = '' %}{{ my_toc | markdownify | strip }}
--------------------------------------------------------------------------------
/docs/pages/redux-ramda.md:
--------------------------------------------------------------------------------
1 | # Redux patterns
2 |
3 | ## Patterns
4 |
5 | ### 1. Selectors
6 |
7 | ```js
8 | const items = (state) => fromEntities.getItems(state.entities)
9 | ```
10 |
11 | ```js
12 | const items = R.o(fromEntities.getItems, R.path(['entities']);
13 |
14 | const isLoading = R.path(['ui', 'loading']);
15 | ```
16 |
17 | ### 2. Memoized Selectors
18 |
19 | ```js
20 | const createSelector = (path) => R.memoizeWith(R.identity, R.path(path))
21 |
22 | // ...
23 |
24 | const isLoading = createSelector(['ui', 'loading'])
25 | ```
26 |
27 | ### 3. Mapping state
28 |
29 | ```js
30 | // Before Ramda
31 | const mapStateToProps = (state, ownProps) => ({
32 | items: getItems(state, ownProps),
33 | isLoading: isLoading(state, ownProps),
34 | })
35 | ```
36 |
37 | ```js
38 | // After Ramda
39 | const mapStateToProps = R.applySpec({
40 | items: getItems,
41 | isLoading: isLoading,
42 | })
43 | ```
44 |
45 | ### 4. Creating action creators
46 |
47 | We want to use following action creators in our application:
48 |
49 | ```js
50 | reset() // { type": "RESET" }
51 | increment(1) // { payload: 1, type: "INCREMENT" }
52 | fetchItems({ items: "some" })
53 | // => { meta: { page: 0 }, payload: "some", type: "FETCH_ITEMS" }
54 | ```
55 |
56 | * `reset` is simpliest action creator - it does not use any arguments
57 | * `increment` takes one argument that represents `payload` of the action
58 | * `fetchItems` is the most complex one:
59 | * computes `payload` from first argument and
60 | * adds `meta` attribute to the action
61 |
62 | ---
63 |
64 | We can introduce factory functions (`createAction`, `createConstantAction`, `createSimpleAction`), that can encapsulates creation of action creators.
65 |
66 | ```js
67 | // Before Ramda
68 | const createAction = (type, getPayload, getMeta) =>
69 | (payload, meta) => ({
70 | type,
71 | payload: getPayload(payload),
72 | meta: getMeta(meta),
73 | });
74 |
75 | const createConstantAction = (type) => createAction(
76 | type,
77 | x => undefined,
78 | () => undefined
79 | );
80 |
81 | const createSimpleAction = (type) => createAction(
82 | type,
83 | x => x,
84 | () => undefined
85 | );
86 |
87 | // ...
88 |
89 | const reset = createConstantAction("RESET")
90 | const increment = createSimpleAction("INCREMENT");
91 | const fetchItems = createAction(
92 | "FETCH_ITEMS",
93 | (x) => x.items,
94 | () => ({ page: 0 })
95 | )
96 | ```
97 |
98 | *Note:* The above code is not quite ideal. Because call `reset()` gives you:
99 | ```js
100 | {"meta": undefined, "payload": undefined, "type": "RESET"}
101 | ```
102 |
103 | Ramda version bellow solves the problem by filtering out unspecified values.
104 |
105 | ```js
106 | // After Ramda
107 | const createAction = R.curry(
108 | (type, getPayload, getMeta) => R.compose(
109 | R.reject(R.isNil),
110 | R.applySpec({
111 | type: R.always(type),
112 | payload: getPayload,
113 | meta: getMeta,
114 | })
115 | )
116 | );
117 | const createSimpleAction = createAction(R.__, R.identity, R.always(null));
118 | const createContantAction = createAction(R.__, R.always(null), R.always(null));
119 |
120 | // ...
121 |
122 | const reset = createContantAction("RESET")
123 | const increment = createSimpleAction("INCREMENT");
124 | const fetchItems = createAction("FETCH_ITEMS", R.prop("items"), R.always({ page: 0 }))
125 | ```
126 |
127 | ### 5. Replacing `switch` inside reducer
128 |
129 | ```js
130 | // Before Ramda:
131 | cosnt initialState = 0;
132 | const counter = (state = initialState, action) =>
133 | switch (action.type) {
134 | case "INCREMENT":
135 | return state + action.payload;
136 | case "RESET":
137 | return initialState;
138 | default:
139 | return state;
140 | }
141 | ```
142 |
143 | ```js
144 | // After Ramda
145 |
146 | // Firstly we introduce `switchReducer` function
147 |
148 | const isUndefined = R.o(R.equals("Undefined"), R.type);
149 | const overHead = R.over(R.lensIndex(0));
150 | const toActionTypeEquals = (type) => R.flip(R.whereEq({ type }));
151 |
152 | const switchReducer = (rs, initialState) => R.compose(
153 | R.cond,
154 | R.prepend([isUndefined, R.always(initialState)]),
155 | R.append([R.T, R.identity]),
156 | R.map(overHead(toActionTypeEquals))
157 | )(rs);
158 |
159 | //...
160 |
161 | // Than we can write every reducer with following convenient API:
162 | const initialState = 1
163 | const counter = switchReducer([
164 | ["INCREMENT", (state, action) => state + action.payload],
165 | ["RESET", R.always(initialState)],
166 | ], initialState);
167 |
168 | // ...
169 |
170 | counter(undefined, {}) // 1
171 | counter(3, { type: "INCREMENT", payload: 2 }) // 5
172 | counter(3, { type: "RESET" }) // 1
173 | counter(3, { type: "LOAD_ITEMS" }) // 3
174 | ```
175 |
176 | ### 6. Local State with `filteredReducer`
177 |
178 | In examples we will use following reducer:
179 |
180 | ```js
181 | const add = (state = 0, action) => action.type === "INCREMENT" ? state + 1 : state
182 | ```
183 |
184 | Lets see following code as an introduction to the problem:
185 |
186 | ```js
187 |
188 | const root = combineReducers({
189 | widgetA: add,
190 | widgetB: add
191 | })
192 |
193 | // { widgetA: 0, widgetB: 0 }
194 |
195 | store.dispatch({ type: "INCREMENT" })
196 | // { widgetA: 1, widgetB: 1 }
197 |
198 | ```
199 |
200 | As we can see, after "INCREMENT" action, every slice of the state, that is managed by `add` reducer, will be incremented.
201 | Following pattern solves the problem of how to target the specific slice of state.
202 |
203 | Following most verbose solution uses `action.meta` to determine if the `add` reducer should be called:
204 |
205 | ```js
206 | const root = combineReducers({
207 | widgetA: (state, action) =>
208 | action.meta && action.meta.namespace === "@WIDGET-A" ? add(state, action) : state,
209 | widgetB:(state, action) =>
210 | action.meta && action.meta.namespace === "@WIDGET-B" ? add(state, action) : state,
211 | })
212 |
213 | // { widgetA: 0, widgetB: 0 }
214 |
215 | store.dispatch({ type: "INCREMENT", meta: { namespace: "@WIDGET-A" } })
216 | // { widgetA: 1, widgetB: 0 }
217 |
218 |
219 | store.dispatch({ type: "INCREMENT", meta: { namespace: "@WIDGET-B" } })
220 | // { widgetA: 1, widgetB: 1 }
221 | ```
222 |
223 | Next, we introduce `filteredReducer` function:
224 |
225 | ```js
226 | // Before Ramda
227 | const filteredReducer = (pred, reducer) =>
228 | (state, action) =>
229 | pred(action) ? reducer(state, action) : state;
230 |
231 | const namespaceEquals = (ns) => (action) => action.meta && action.meta.namespace === ns
232 |
233 | // ...
234 |
235 | const root = combineReducers({
236 | widgetA: filteredReducer(namespaceEquals("@WIDGET-A"), add),
237 | widgetB: filteredReducer(namespaceEquals("@WIDGET-B"), add),
238 | global: add,
239 | })
240 | ```
241 |
242 | ```js
243 | // After Ramda
244 | const filteredReducer = (pred, reducer) => R.cond([
245 | [R.flip(pred), reducer],
246 | [R.T, R.nthArg(0)]
247 | ])
248 |
249 | const namespaceEquals = R.pathEq(["meta", "namespace"])
250 |
251 | // ...
252 |
253 | const root = combineReducers({
254 | widgetA: filteredReducer(namespaceEquals("@WIDGET-A"), add),
255 | widgetB: filteredReducer(namespaceEquals("@WIDGET-B"), add),
256 | global: add,
257 | })
258 | ```
259 |
--------------------------------------------------------------------------------
/docs/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | specs:
3 | activesupport (4.2.9)
4 | i18n (~> 0.7)
5 | minitest (~> 5.1)
6 | thread_safe (~> 0.3, >= 0.3.4)
7 | tzinfo (~> 1.1)
8 | addressable (2.5.2)
9 | public_suffix (>= 2.0.2, < 4.0)
10 | coffee-script (2.4.1)
11 | coffee-script-source
12 | execjs
13 | coffee-script-source (1.11.1)
14 | colorator (1.1.0)
15 | commonmarker (0.17.9)
16 | ruby-enum (~> 0.5)
17 | concurrent-ruby (1.0.5)
18 | em-websocket (0.5.1)
19 | eventmachine (>= 0.12.9)
20 | http_parser.rb (~> 0.6.0)
21 | ethon (0.11.0)
22 | ffi (>= 1.3.0)
23 | eventmachine (1.2.5)
24 | execjs (2.7.0)
25 | faraday (0.14.0)
26 | multipart-post (>= 1.2, < 3)
27 | ffi (1.9.23)
28 | forwardable-extended (2.6.0)
29 | gemoji (3.0.0)
30 | github-pages (181)
31 | activesupport (= 4.2.9)
32 | github-pages-health-check (= 1.4.0)
33 | jekyll (= 3.7.3)
34 | jekyll-avatar (= 0.5.0)
35 | jekyll-coffeescript (= 1.1.1)
36 | jekyll-commonmark-ghpages (= 0.1.5)
37 | jekyll-default-layout (= 0.1.4)
38 | jekyll-feed (= 0.9.3)
39 | jekyll-gist (= 1.5.0)
40 | jekyll-github-metadata (= 2.9.4)
41 | jekyll-mentions (= 1.3.0)
42 | jekyll-optional-front-matter (= 0.3.0)
43 | jekyll-paginate (= 1.1.0)
44 | jekyll-readme-index (= 0.2.0)
45 | jekyll-redirect-from (= 0.13.0)
46 | jekyll-relative-links (= 0.5.3)
47 | jekyll-remote-theme (= 0.2.3)
48 | jekyll-sass-converter (= 1.5.2)
49 | jekyll-seo-tag (= 2.4.0)
50 | jekyll-sitemap (= 1.2.0)
51 | jekyll-swiss (= 0.4.0)
52 | jekyll-theme-architect (= 0.1.1)
53 | jekyll-theme-cayman (= 0.1.1)
54 | jekyll-theme-dinky (= 0.1.1)
55 | jekyll-theme-hacker (= 0.1.1)
56 | jekyll-theme-leap-day (= 0.1.1)
57 | jekyll-theme-merlot (= 0.1.1)
58 | jekyll-theme-midnight (= 0.1.1)
59 | jekyll-theme-minimal (= 0.1.1)
60 | jekyll-theme-modernist (= 0.1.1)
61 | jekyll-theme-primer (= 0.5.3)
62 | jekyll-theme-slate (= 0.1.1)
63 | jekyll-theme-tactile (= 0.1.1)
64 | jekyll-theme-time-machine (= 0.1.1)
65 | jekyll-titles-from-headings (= 0.5.1)
66 | jemoji (= 0.9.0)
67 | kramdown (= 1.16.2)
68 | liquid (= 4.0.0)
69 | listen (= 3.1.5)
70 | mercenary (~> 0.3)
71 | minima (= 2.4.0)
72 | nokogiri (>= 1.8.1, < 2.0)
73 | rouge (= 2.2.1)
74 | terminal-table (~> 1.4)
75 | github-pages-health-check (1.4.0)
76 | addressable (~> 2.3)
77 | net-dns (~> 0.8)
78 | octokit (~> 4.0)
79 | public_suffix (~> 2.0)
80 | typhoeus (~> 1.3)
81 | html-pipeline (2.7.1)
82 | activesupport (>= 2)
83 | nokogiri (>= 1.4)
84 | http_parser.rb (0.6.0)
85 | i18n (0.9.5)
86 | concurrent-ruby (~> 1.0)
87 | jekyll (3.7.3)
88 | addressable (~> 2.4)
89 | colorator (~> 1.0)
90 | em-websocket (~> 0.5)
91 | i18n (~> 0.7)
92 | jekyll-sass-converter (~> 1.0)
93 | jekyll-watch (~> 2.0)
94 | kramdown (~> 1.14)
95 | liquid (~> 4.0)
96 | mercenary (~> 0.3.3)
97 | pathutil (~> 0.9)
98 | rouge (>= 1.7, < 4)
99 | safe_yaml (~> 1.0)
100 | jekyll-avatar (0.5.0)
101 | jekyll (~> 3.0)
102 | jekyll-coffeescript (1.1.1)
103 | coffee-script (~> 2.2)
104 | coffee-script-source (~> 1.11.1)
105 | jekyll-commonmark (1.2.0)
106 | commonmarker (~> 0.14)
107 | jekyll (>= 3.0, < 4.0)
108 | jekyll-commonmark-ghpages (0.1.5)
109 | commonmarker (~> 0.17.6)
110 | jekyll-commonmark (~> 1)
111 | rouge (~> 2)
112 | jekyll-default-layout (0.1.4)
113 | jekyll (~> 3.0)
114 | jekyll-feed (0.9.3)
115 | jekyll (~> 3.3)
116 | jekyll-gist (1.5.0)
117 | octokit (~> 4.2)
118 | jekyll-github-metadata (2.9.4)
119 | jekyll (~> 3.1)
120 | octokit (~> 4.0, != 4.4.0)
121 | jekyll-mentions (1.3.0)
122 | activesupport (~> 4.0)
123 | html-pipeline (~> 2.3)
124 | jekyll (~> 3.0)
125 | jekyll-optional-front-matter (0.3.0)
126 | jekyll (~> 3.0)
127 | jekyll-paginate (1.1.0)
128 | jekyll-readme-index (0.2.0)
129 | jekyll (~> 3.0)
130 | jekyll-redirect-from (0.13.0)
131 | jekyll (~> 3.3)
132 | jekyll-relative-links (0.5.3)
133 | jekyll (~> 3.3)
134 | jekyll-remote-theme (0.2.3)
135 | jekyll (~> 3.5)
136 | rubyzip (>= 1.2.1, < 3.0)
137 | typhoeus (>= 0.7, < 2.0)
138 | jekyll-sass-converter (1.5.2)
139 | sass (~> 3.4)
140 | jekyll-seo-tag (2.4.0)
141 | jekyll (~> 3.3)
142 | jekyll-sitemap (1.2.0)
143 | jekyll (~> 3.3)
144 | jekyll-swiss (0.4.0)
145 | jekyll-theme-architect (0.1.1)
146 | jekyll (~> 3.5)
147 | jekyll-seo-tag (~> 2.0)
148 | jekyll-theme-cayman (0.1.1)
149 | jekyll (~> 3.5)
150 | jekyll-seo-tag (~> 2.0)
151 | jekyll-theme-dinky (0.1.1)
152 | jekyll (~> 3.5)
153 | jekyll-seo-tag (~> 2.0)
154 | jekyll-theme-hacker (0.1.1)
155 | jekyll (~> 3.5)
156 | jekyll-seo-tag (~> 2.0)
157 | jekyll-theme-leap-day (0.1.1)
158 | jekyll (~> 3.5)
159 | jekyll-seo-tag (~> 2.0)
160 | jekyll-theme-merlot (0.1.1)
161 | jekyll (~> 3.5)
162 | jekyll-seo-tag (~> 2.0)
163 | jekyll-theme-midnight (0.1.1)
164 | jekyll (~> 3.5)
165 | jekyll-seo-tag (~> 2.0)
166 | jekyll-theme-minimal (0.1.1)
167 | jekyll (~> 3.5)
168 | jekyll-seo-tag (~> 2.0)
169 | jekyll-theme-modernist (0.1.1)
170 | jekyll (~> 3.5)
171 | jekyll-seo-tag (~> 2.0)
172 | jekyll-theme-primer (0.5.3)
173 | jekyll (~> 3.5)
174 | jekyll-github-metadata (~> 2.9)
175 | jekyll-seo-tag (~> 2.0)
176 | jekyll-theme-slate (0.1.1)
177 | jekyll (~> 3.5)
178 | jekyll-seo-tag (~> 2.0)
179 | jekyll-theme-tactile (0.1.1)
180 | jekyll (~> 3.5)
181 | jekyll-seo-tag (~> 2.0)
182 | jekyll-theme-time-machine (0.1.1)
183 | jekyll (~> 3.5)
184 | jekyll-seo-tag (~> 2.0)
185 | jekyll-titles-from-headings (0.5.1)
186 | jekyll (~> 3.3)
187 | jekyll-watch (2.0.0)
188 | listen (~> 3.0)
189 | jemoji (0.9.0)
190 | activesupport (~> 4.0, >= 4.2.9)
191 | gemoji (~> 3.0)
192 | html-pipeline (~> 2.2)
193 | jekyll (~> 3.0)
194 | kramdown (1.16.2)
195 | liquid (4.0.0)
196 | listen (3.1.5)
197 | rb-fsevent (~> 0.9, >= 0.9.4)
198 | rb-inotify (~> 0.9, >= 0.9.7)
199 | ruby_dep (~> 1.2)
200 | mercenary (0.3.6)
201 | mini_portile2 (2.3.0)
202 | minima (2.4.0)
203 | jekyll (~> 3.5)
204 | jekyll-feed (~> 0.9)
205 | jekyll-seo-tag (~> 2.1)
206 | minitest (5.11.3)
207 | multipart-post (2.0.0)
208 | net-dns (0.8.0)
209 | nokogiri (1.8.2)
210 | mini_portile2 (~> 2.3.0)
211 | octokit (4.8.0)
212 | sawyer (~> 0.8.0, >= 0.5.3)
213 | pathutil (0.16.1)
214 | forwardable-extended (~> 2.6)
215 | public_suffix (2.0.5)
216 | rb-fsevent (0.10.3)
217 | rb-inotify (0.9.10)
218 | ffi (>= 0.5.0, < 2)
219 | rouge (2.2.1)
220 | ruby-enum (0.7.2)
221 | i18n
222 | ruby_dep (1.5.0)
223 | rubyzip (1.2.1)
224 | safe_yaml (1.0.4)
225 | sass (3.5.6)
226 | sass-listen (~> 4.0.0)
227 | sass-listen (4.0.0)
228 | rb-fsevent (~> 0.9, >= 0.9.4)
229 | rb-inotify (~> 0.9, >= 0.9.7)
230 | sawyer (0.8.1)
231 | addressable (>= 2.3.5, < 2.6)
232 | faraday (~> 0.8, < 1.0)
233 | terminal-table (1.8.0)
234 | unicode-display_width (~> 1.1, >= 1.1.1)
235 | thread_safe (0.3.6)
236 | typhoeus (1.3.0)
237 | ethon (>= 0.9.0)
238 | tzinfo (1.2.5)
239 | thread_safe (~> 0.1)
240 | unicode-display_width (1.3.0)
241 |
242 | PLATFORMS
243 | ruby
244 |
245 | DEPENDENCIES
246 | github-pages
247 |
248 | BUNDLED WITH
249 | 1.16.1
250 |
--------------------------------------------------------------------------------
/docs/pages/react-ramda.md:
--------------------------------------------------------------------------------
1 | # React patterns
2 |
3 | ## Introduction
4 |
5 | ### React Components
6 |
7 | We will speak about properties of React Component as `Props`. But in JS terms properties are just an plain object.
8 |
9 | For more compact syntax instead of `React.Component`, `React.Element` we will write `Component`, `Element`.
10 |
11 | In general, we will work with functional stateless components - pure functions that takes `Props` as an argument and returns JSX/`Element`:
12 |
13 | ```
14 | React.Component :: Props -> Element
15 | ```
16 |
17 |
18 | ### High-Order Component (HOC)
19 |
20 | A Higher-Order Component is a function that accepts a `Component` as an argument and returns another `Component`:
21 |
22 | ```
23 | Component -> Component
24 | ```
25 |
26 | **Example of identity HOC:**
27 |
28 | ```jsx
29 | const identity = (NextComponent) => (props) =>
30 |
31 | const MyContainer = () => "I am MyContainer";
32 |
33 | const NewContainer = identity(MyContainer);
34 | ```
35 |
36 | In example above `identity` HOC creates `NewContainer` which renders exactly the same output as the `MyContainer`.
37 |
38 | **Note:** The term originates from [_high-order function_](https://en.wikipedia.org/wiki/Higher-order_function).
39 |
40 | **Note:** Sometimes HOCs are called _component decorators_. [See what decorators really are](https://github.com/tc39/proposal-decorators) and why they are occasionally mismatched with term _high-Order component_.
41 |
42 | ### Examples of HOCs
43 |
44 | **Collections of HOCs**
45 |
46 | * [acdlite/recompose](https://github.com/acdlite/recompose)
47 | * [kriasoft/eact-decorators](https://github.com/kriasoft/react-decorators)
48 | * [jaredpalmer/react-fns](https://github.com/jaredpalmer/react-fns)
49 | * [klarna/higher-Order-components](https://github.com/klarna/higher-Order-components)
50 |
51 | **Providing a context of libraries**
52 |
53 | In many cases is HOC used for adding functionality to your component from certain library via React context API.
54 |
55 | See:
56 |
57 | * [reactjs/redux](https://github.com/reactjs/redux) - `connect`
58 | * [yahoo/react-intl](https://github.com/yahoo/react-intl) - `injectIntl` HOC
59 | * [erikras/redux-form](https://github.com/erikras/redux-form) - `reduxForm`
60 |
61 | ## Patterns
62 |
63 | ### 1. Static Component
64 |
65 |
66 | ```jsx
67 | // Before Ramda:
68 | const Loading = () => "Loading...";
69 |
70 | ReactDOM.render(, rootEl) // renders "Loading..."
71 | ```
72 |
73 |
74 | ```jsx
75 | // After Ramda:
76 | const Loading = R.always("Loading...");
77 |
78 | ReactDOM.render(, rootEl) // renders "Loading..."
79 | ```
80 |
81 |
82 | ### 2. Composition of High-Order components
83 |
84 | Use function composition instead of nesting calls.
85 |
86 | ---
87 |
88 | ```js
89 | // Before Ramda
90 | connect()(
91 | reduxForm()(
92 | injectIntl(Container)
93 | )
94 | )
95 | ```
96 |
97 | ```js
98 | // After Ramda
99 | R.compose(
100 | connect(),
101 | reduxForm(),
102 | injectIntl
103 | )(Container)
104 |
105 | // or
106 |
107 | R.pipe(
108 | injectIntl,
109 | reduxForm(),
110 | connect()
111 | )(Container)
112 | ```
113 |
114 | If you are composing from exactly two HOCs, you can use `R.o`.
115 |
116 | It is highly recommended to use just one of `R.o`, `R.compose`, `R.pipe` in the scope of your Application for composing HOCs.
117 |
118 | ### 3. Branching with `R.ifElse`
119 |
120 | Use `R.ifElse` for conditional render.
121 |
122 | ---
123 |
124 | Lets define following components:
125 |
126 | ```jsx
127 | const Loading = R.always("Loading...");
128 | const Section = ({ content }) => ;
129 | ```
130 |
131 | Than define HOC of conditional render:
132 |
133 | ```jsx
134 | // Before Ramda:
135 | const Content = (props) => props.loading ?
136 | :
137 | ;
138 | ```
139 |
140 | ```jsx
141 | // After Ramda:
142 | const withLoading = R.ifElse(R.prop('loading'), Loading)
143 |
144 | const Content = withLoading(Section)
145 | ```
146 |
147 | In this example `withLoading` HOC can be simply reused for all your components with `loading` property.
148 |
149 | ### 4. Branching with `R.cond`
150 |
151 | Use `R.cond` for conditional render.
152 |
153 | ---
154 |
155 | Lets define following components:
156 |
157 | ```jsx
158 | const Loading = R.always("Loading...");
159 | const Missing = R.always("No results.");
160 | const Section = ({ content }) => ;
161 | ```
162 |
163 | Than define HOC of conditional render:
164 |
165 | ```jsx
166 | // Before Ramda:
167 | const Content = (props) => {
168 | if (props.loading) {
169 | return ;
170 | }
171 | if (!props.items.length) {
172 | return ;
173 | }
174 |
175 | return
176 | }
177 | ```
178 |
179 | ```jsx
180 | // After Ramda:
181 | const Content = R.cond([
182 | [R.prop('loading'), Loading],
183 | [R.isEmpty('items'), Missing],
184 | [R.T, Section],
185 | ]);
186 | ```
187 |
188 | ### 5. Mapping properties with `mapProps`
189 |
190 | Lets define following functions:
191 |
192 | ```js
193 | // Props -> Component -> Element
194 | const createElement = R.curryN(2, React.createElement);
195 |
196 | // mapProps :: (a -> a) -> Component -> Component
197 | const mapProps = R.flip(R.useWith(R.o, [createElement, R.identity]));
198 | ```
199 |
200 | See Appendix for process of refactor to pointfreee version of `mapProps`.
201 |
202 | With `mapProps` you can transform properties of final component with your custom mapping functions:
203 |
204 | ```jsx
205 | const Section = ({ heading, children, ...rest }) => (
206 |
207 | {heading}
208 | {children}
209 |
210 | );
211 |
212 |
213 | const SectionWithUpperHeading = mapProps(
214 | (props) => ({ ...props, heading: R.toUpper(props.heading) })
215 | )(Section)
216 | ```
217 |
218 |
219 | Follows few examples with `mapProps`.
220 |
221 | #### 5.1 Transforming properites with `R.evolve`
222 |
223 | Assuming component `Section` from section 5:
224 |
225 | ```jsx
226 | const EnhancedSection = mapProps(
227 | R.evolve({ heading: R.toUpper })
228 | )(Section)
229 |
230 | // ...
231 |
232 |
233 | Ramda is awesome!
234 |
235 |
236 | // renders to:
237 |
238 | TRUTH ABOUT RAMDA
239 | Ramda is awesome!
240 |
241 | ```
242 |
243 | #### 5.2 Adding props with `computeProps`
244 |
245 | Lets introduce function `computeProps` (see Appendix section):
246 |
247 | ```js
248 | const computeProps = R.converge(R.merge, [R.nthArg(1), R.call]);
249 | ```
250 |
251 | Assuming following contrieved component `SimpleSection`:
252 |
253 | ```jsx
254 | const SimpleSection = ({
255 | heading,
256 | childrenLength,
257 | children,
258 | ...rest
259 | }) => (
260 |
261 | {heading}
262 | {children}
263 | {childrenLength}
264 |
265 | );
266 | ```
267 |
268 | ```jsx
269 | const EnhancedSection = mapProps(
270 | ({ children }) => ({ childrenLength: R.length(children) })
271 | )(SimpleSection)
272 |
273 | // ...
274 |
275 |
276 | Ramda is awesome!
277 |
278 |
279 | // renders to:
280 |
281 | Truth about Ramda
282 | Ramda is awesome!
283 | 17
284 |
285 | ```
286 |
287 | #### 5.3 Picking properties with `R.pick`
288 |
289 | Assuming component `Section` from section 5:
290 |
291 | ```jsx
292 | const EnhancedSection = mapProps(
293 | R.pick(["className", "children", "heading"])
294 | )(Section)
295 |
296 | // ...
297 |
298 |
303 | Ramda is awesome!
304 |
305 |
306 | // renders to:
307 |
308 | Truth about Ramda
309 | Ramda is awesome!
310 |
311 | ```
312 |
313 | ### 6. Defining [`displayName`](https://reactjs.org/docs/react-component.html#displayname) with `setDisplayName`
314 |
315 | Lets define following HOC `setDisplayName`:
316 |
317 | ```jsx
318 | const setDisplayName = (displayName) => R.tap(
319 | (C) => C.displayName = displayName
320 | );
321 | ```
322 |
323 | ## Appendix
324 |
325 | ### Pointfree `mapProps`
326 |
327 | ```js
328 | const mapProps = R.curry(
329 | (mapping, C) => (props) =>
330 | );
331 |
332 | // Replacing JSX
333 | const mapProps = R.curry(
334 | (mapping, C) => (props) => React.createElement(C, mapping(props))
335 | );
336 |
337 | // Introducting curried version of React.createElement
338 | const createElement = R.curryN(2, React.createElement);
339 |
340 | const mapProps = R.curry(
341 | (mapping, C) => (props) => renderComponent(C)(mapping(props))
342 | );
343 |
344 | // R.o for functional composition
345 | const mapProps = R.curry(
346 | (mapping, C) => (props) => R.o(renderComponent(C), mapping)(props)
347 | );
348 |
349 | // removing explicit argument `props`
350 | const mapProps = R.curry(
351 | (mapping, C) => R.o(renderComponent(C), mapping)
352 | );
353 |
354 | // final pointfree version with R.flip
355 | const mapProps = R.flip(R.useWith(R.o, [renderComponent, R.identity]));
356 | ```
357 |
358 | ### Pointfree `computeProps`
359 |
360 | ```js
361 | const computeProps = R.curry(
362 | (props, mapping) => R.merge(props, mapping(props))
363 | );
364 |
365 | // Adding converge is pretty straigtforward
366 | const computeProps = R.converge(R.merge, [R.nthArg(1), R.call]);
367 | ```
368 |
369 |
--------------------------------------------------------------------------------