25 | {/*
26 | Instead of providing a static representation of what renders,
27 | use the `render` prop to dynamically determine what to render.
28 | */}
29 | {this.props.render(this.state)}
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/playground/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import { RootAction, RootState, Services } from 'MyTypes';
2 | import { applyMiddleware, createStore } from 'redux';
3 | import { createEpicMiddleware } from 'redux-observable';
4 |
5 | import services from '../services';
6 | import { routerMiddleware } from './redux-router';
7 | import rootEpic from './root-epic';
8 | import rootReducer from './root-reducer';
9 | import { composeEnhancers } from './utils';
10 |
11 | const epicMiddleware = createEpicMiddleware<
12 | RootAction,
13 | RootAction,
14 | RootState,
15 | Services
16 | >({
17 | dependencies: services,
18 | });
19 |
20 | // configure middlewares
21 | const middlewares = [epicMiddleware, routerMiddleware];
22 | // compose enhancers
23 | const enhancer = composeEnhancers(applyMiddleware(...middlewares));
24 |
25 | // rehydrate state on app start
26 | const initialState = {};
27 |
28 | // create store
29 | const store = createStore(
30 | rootReducer,
31 | initialState,
32 | enhancer
33 | );
34 |
35 | epicMiddleware.run(rootEpic);
36 |
37 | // export store singleton instance
38 | export default store;
39 |
--------------------------------------------------------------------------------
/playground/src/features/todos-typesafe/reducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { ActionType, getType } from 'typesafe-actions';
3 |
4 | import { Todo, TodosFilter } from './models';
5 | import * as todos from './actions';
6 |
7 | export type TodosState = Readonly<{
8 | todos: Todo[];
9 | todosFilter: TodosFilter;
10 | }>;
11 |
12 | export type TodosAction = ActionType;
13 |
14 | export default combineReducers({
15 | todos: (state = [], action) => {
16 | switch (action.type) {
17 | case getType(todos.add):
18 | return [...state, action.payload];
19 |
20 | case getType(todos.toggle):
21 | return state.map(item => (item.id === action.payload.id ? { ...item, completed: !item.completed } : item));
22 |
23 | default:
24 | return state;
25 | }
26 | },
27 | todosFilter: (state = TodosFilter.All, action) => {
28 | switch (action.type) {
29 | case getType(todos.changeFilter):
30 | return action.payload;
31 |
32 | default:
33 | return state;
34 | }
35 | },
36 | });
37 |
--------------------------------------------------------------------------------
/playground/src/features/counters/actions.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { action } from 'typesafe-actions';
3 |
4 | import { ADD, INCREMENT } from './constants';
5 |
6 | /* SIMPLE API */
7 |
8 | export const increment = () => action(INCREMENT);
9 | export const add = (amount: number) => action(ADD, amount);
10 |
11 | /* ADVANCED API */
12 |
13 | // More flexible allowing to create complex actions more easily
14 | // use can use "action-creator" instance in place of "type constant"
15 | // e.g. case getType(increment): return action.payload;
16 | // This will allow to completely eliminate need for "constants" in your application, more info here:
17 | // https://github.com/piotrwitek/typesafe-actions#constants
18 |
19 | import { createAction } from 'typesafe-actions';
20 | import { Todo } from '../todos/models';
21 |
22 | export const emptyAction = createAction(INCREMENT)();
23 | export const payloadAction = createAction(ADD)();
24 | export const payloadMetaAction = createAction(ADD)();
25 |
26 | export const payloadCreatorAction = createAction(
27 | 'TOGGLE_TODO',
28 | (todo: Todo) => todo.id
29 | )();
30 |
--------------------------------------------------------------------------------
/playground/src/hooks/use-reducer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | interface State {
4 | count: number;
5 | }
6 |
7 | type Action = { type: 'reset' } | { type: 'increment' } | { type: 'decrement' };
8 |
9 | function reducer(state: State, action: Action): State {
10 | switch (action.type) {
11 | case 'increment':
12 | return { count: state.count + 1 };
13 | case 'decrement':
14 | return { count: state.count - 1 };
15 | case 'reset':
16 | return { count: 0 };
17 | default:
18 | throw new Error();
19 | }
20 | }
21 |
22 | interface CounterProps {
23 | initialCount: number;
24 | }
25 |
26 | function Counter({ initialCount }: CounterProps) {
27 | const [state, dispatch] = React.useReducer(reducer, {
28 | count: initialCount,
29 | });
30 |
31 | return (
32 | <>
33 | Count: {state.count}
34 |
35 |
36 |
37 | >
38 | );
39 | }
40 |
41 | export default Counter;
42 |
--------------------------------------------------------------------------------
/playground/src/routes/home.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FCCounterUsage from '../components/fc-counter.usage';
3 | import FCCounterWithDefaultPropsUsage from '../components/fc-counter-with-default-props.usage';
4 | import FCSpreadAttributesUsage from '../components/fc-spread-attributes.usage';
5 | import ClassCounterUsage from '../components/class-counter.usage';
6 | import ClassCounterWithDefaultPropsUsage from '../components/class-counter-with-default-props.usage';
7 | import UserListUsage from '../components/generic-list.usage';
8 | import WithErrorBoundaryUsage from '../hoc/with-error-boundary.usage';
9 | import WithStateUsage from '../hoc/with-state.usage';
10 | import WithConnectedCountUsage from '../hoc/with-connected-count.usage';
11 |
12 | export function Home() {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Piotr Witek (http://piotrwitek.github.io)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guide
2 |
3 | ## General
4 | 1. Make sure you have read and understand the **Goals** section to be aligned with project goals.
5 | 2. Before submitting a PR please comment in the relevant issue (or create a new one if it doesn't exist yet) to discuss all the requirements (this will prevent rejecting the PR and wasting your work).
6 | 3. All workflow scripts (prettier, linter, tests) must pass successfully (it is run automatically on CI and will fail on github checks).
7 |
8 | ## Edit `README_SOURCE.md` to generate an updated `README.md`
9 | Don't edit `README.md` directly - it is generated automatically from `README_SOURCE.md` using an automated script.
10 | - Use `sh ./generate-readme.sh` script to generate updated `README.md` (this will inject code examples using type-checked source files from the `/playground` folder)
11 | - So to make changes in code examples edit source files in `/playground` folder
12 |
13 | **Source code inject directives:**
14 | ```
15 | # Inject code block with highlighter
16 | ::codeblock='playground/src/components/fc-counter.tsx'::
17 |
18 | # Inject code block with highlighter and expander
19 | ::expander='playground/src/components/fc-counter.usage.tsx'::
20 | ```
21 |
--------------------------------------------------------------------------------
/playground/src/app.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { Outlet, Route, Routes } from 'react-router-dom';
4 | import { ReduxRouter } from '@lagunovsky/redux-react-router'
5 |
6 | import { Layout } from './layout/layout';
7 | import { LayoutFooter } from './layout/layout-footer';
8 | import { LayoutHeader } from './layout/layout-header';
9 | import { Home } from './routes/home';
10 | import { NotFound } from './routes/not-found';
11 | import { history, store } from './store';
12 |
13 | export function App() {
14 | return (
15 |
16 |
17 |
18 | }
23 | renderFooter={() => }
24 | renderContent={() => }
25 | />
26 | }
27 | >
28 | } />
29 | } />
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/playground/src/features/todos/reducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { ActionType } from 'typesafe-actions';
3 |
4 | import { Todo, TodosFilter } from './models';
5 | import * as actions from './actions';
6 | import { ADD, CHANGE_FILTER, TOGGLE } from './constants';
7 |
8 | export type TodosAction = ActionType;
9 |
10 | export type TodosState = Readonly<{
11 | todos: Todo[];
12 | todosFilter: TodosFilter;
13 | }>;
14 | const initialState: TodosState = {
15 | todos: [],
16 | todosFilter: TodosFilter.All,
17 | };
18 |
19 | export default combineReducers({
20 | todos: (state = initialState.todos, action) => {
21 | switch (action.type) {
22 | case ADD:
23 | return [...state, action.payload];
24 |
25 | case TOGGLE:
26 | return state.map(item =>
27 | item.id === action.payload
28 | ? { ...item, completed: !item.completed }
29 | : item
30 | );
31 |
32 | default:
33 | return state;
34 | }
35 | },
36 | todosFilter: (state = initialState.todosFilter, action) => {
37 | switch (action.type) {
38 | case CHANGE_FILTER:
39 | return action.payload;
40 |
41 | default:
42 | return state;
43 | }
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/playground/src/models/nominal-types.ts:
--------------------------------------------------------------------------------
1 | // Nominal Typing
2 | // Usefull to model domain concepts that are using primitive data type for it's value
3 |
4 | // Method 1: using "interface"
5 | export interface Name extends String {
6 | _brand: 'Name';
7 | }
8 | const createName = (name: string): Name => {
9 | // validation of business rules
10 | return name as any;
11 | };
12 |
13 | // Method 2: using "type"
14 | type Surname = string & { _brand: 'Surname' };
15 | const createSurname = (surname: string): Surname => {
16 | // validation of business rules
17 | return surname as any;
18 | };
19 |
20 | type Person = {
21 | name: Name;
22 | surname: Surname;
23 | };
24 |
25 | const person: Person = {
26 | name: createName('Piotr'),
27 | surname: createSurname('Witek'),
28 | };
29 |
30 | // Type system will ensure that the domain objects can only contain correct data
31 | // person.name = 'Karol'; // error
32 | // person.name = person.surname; // error
33 | person.name = createName('Karol'); // OK!
34 | // person.surname = 'Mate'; // error
35 | // person.surname = person.name; // error
36 | person.surname = createSurname('Mate'); // OK!
37 |
38 | // easy casting to supertype
39 | export let str: string;
40 | str = person.name.toString(); // Method 1 & Method 2
41 | str = person.surname; // Method 2 only
42 |
--------------------------------------------------------------------------------
/generate-readme.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const ROOT_PATH = `${__dirname}/`;
4 | const inputFiles = [ROOT_PATH + 'README_SOURCE.md'];
5 | const outputFile = ROOT_PATH + 'README.md';
6 |
7 | const result = inputFiles
8 | .map(filePath => fs.readFileSync(filePath, 'utf8'))
9 | .map(injectCodeBlocks)
10 | .map(injectExpanders)
11 | .toString();
12 |
13 | fs.writeFileSync(outputFile, result, 'utf8');
14 |
15 | function injectCodeBlocks(text) {
16 | const regex = /::codeblock='(.+?)'::/g;
17 | return text.replace(regex, createMatchReplacer(withSourceWrapper));
18 | }
19 |
20 | function injectExpanders(text) {
21 | const regex = /::expander='(.+?)'::/g;
22 | return text.replace(regex, createMatchReplacer(withDetailsWrapper));
23 | }
24 |
25 | function createMatchReplacer(wrapper) {
26 | return (match, filePath) => {
27 | console.log(ROOT_PATH + filePath);
28 | const text = fs.readFileSync(ROOT_PATH + filePath, 'utf8');
29 | return wrapper(text);
30 | };
31 | }
32 |
33 | function withSourceWrapper(text) {
34 | return `
35 | ${'```tsx'}
36 | ${text}
37 | ${'```'}
38 | `.trim();
39 | }
40 |
41 | function withDetailsWrapper(text) {
42 | return `
43 | Click to expand