Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
8fcd1e7
working setup
Feb 18, 2019
d59d7a4
move index html to top level folder
npeham Feb 23, 2019
5fb6659
fix quotemark
npeham Feb 23, 2019
1a24ff1
initial router setup
npeham Mar 3, 2019
3655322
move express to devDependencies
npeham Mar 3, 2019
5e68c6a
resolve merge conflicts
npeham Mar 3, 2019
09d31b5
resoolve merge conflicts
npeham Mar 7, 2019
d00ccfc
add private routes handling and add key, add post routes
npeham Mar 16, 2019
dac70b0
resolve merge conflicts with master (enyzme)
npeham Mar 16, 2019
7b7e892
link react-router dependency
npeham Mar 24, 2019
1d416a2
change general structure
npeham Mar 24, 2019
57aab87
instructions: how to add a new scene
npeham Mar 24, 2019
15e0cfa
change numeration
npeham Mar 24, 2019
88315a1
edit instructions
npeham Mar 24, 2019
6722a6e
add strictNullChecks to tsconfig.json
npeham Mar 24, 2019
a436816
change instructions
npeham Mar 24, 2019
7da3e9e
change instructions
npeham Mar 24, 2019
cb7852e
Update README.md
npeham Mar 24, 2019
13f961f
Update README.md
npeham Mar 24, 2019
637c1c9
Update README.md
npeham Mar 24, 2019
7267951
working routing setup
npeham Mar 24, 2019
54a9d10
remove comments
npeham Mar 24, 2019
f07b1df
wrap with spans
npeham Mar 24, 2019
3173026
Merge branch 'react-router' of github.com:npeham/react-typescript-sta…
npeham Mar 24, 2019
e5f5ef1
Update README.md
npeham Mar 24, 2019
a20f5d5
Update README.md
npeham Mar 24, 2019
b5f3ffd
Update README.md
npeham Mar 24, 2019
5ec6eb1
add comment
npeham Apr 21, 2019
daa2264
Update README.md
npeham Apr 21, 2019
2e03f3a
Update README.md
npeham Apr 21, 2019
79326db
create initial test and helper function (setup)
npeham Apr 21, 2019
5b55b89
implement getRoutePath and defaultParams for scenes
npeham Apr 21, 2019
e9856f0
throw error if path is not unique
npeham Apr 21, 2019
f3d1343
refactor rename test decriptions
npeham Apr 21, 2019
cf51c7c
refactoring
npeham Apr 21, 2019
5f9ea26
Merge branch 'react-router' of github.com:npeham/react-typescript-sta…
npeham Apr 21, 2019
3e8797e
refactor move scene dependend code into their file
npeham Apr 21, 2019
ca8d7c3
implement mergeParamsWithDefault and refactor move code
npeham Apr 21, 2019
74b317c
remove mergeParamsWithDefault and move it to more generic object helper
npeham Apr 21, 2019
4e669e8
refactoring destruct params
npeham Apr 21, 2019
b761a73
fix import
npeham Apr 21, 2019
efb8413
remove mergeParamsWithDefault of routing.helper
npeham Apr 21, 2019
c9a197c
refactor rename defaultParams to paramsSchema and dont export it
npeham Apr 21, 2019
4c51c2a
format
npeham Apr 21, 2019
6c73545
remove unused import
npeham Apr 21, 2019
4891e26
Update README.md
npeham Apr 21, 2019
3072b10
Update README.md
npeham Apr 21, 2019
b7c3987
refactor
npeham Apr 21, 2019
690e452
Merge branch 'react-router' of github.com:npeham/react-typescript-sta…
npeham Apr 21, 2019
39ee055
Update README.md
npeham Apr 22, 2019
a2c140e
Update README.md
npeham Apr 22, 2019
27e0494
Update README.md
npeham Apr 22, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,115 @@
# React Typescript Starter

## Dependencies
* Enzyme
* [enzyme](https://airbnb.io/enzyme/)
* [react-router](https://github.com/ReactTraining/react-router)

### Enzyme
## Enzyme
[Enzyme](https://airbnb.io/enzyme/) was set up with [Using Jest with TypeScript](https://basarat.gitbooks.io/typescript/docs/testing/jest.html) (describes how to setup both jest and enzyme).

## Routing

### Create a scene
**Note:** To simplify things I will use the already created `UserEdit` scene for the following instructions.

How to type-safe create and register a new scene in 6 easy steps:
1. Create a new `UserEdit.scene.tsx` file in `modules/user/scenes`
2. Define a interface for the scene params if there are any and export it. It is important that you extend from the interface `SceneParams`. Here is the actual code:
```
export interface UserEditSceneParams extends SceneParams {
userId: string;
projId: string;
}
```
3. Now define and export `paramsSchema` with the type `UserEditSceneParams`:
```
const paramsSchema: UserEditSceneParams = {
projId: '',
userId: '',
};
```
Honestly this only exist to avoid defining the path of a route with params like: `'/user/edit/:projId/:userId/'`. The final path will be auto-generated with the Route enum and the params schema using the helper funtion [`getRoutePath`](https://github.com/npeham/react-typescript-starter/blob/react-router/src/shared/helper/routing/routing.helper.ts#L7) that get called in the [`mapRoutes`](https://github.com/npeham/react-typescript-starter/blob/react-router/src/App.router.tsx#L11) function. With this approach you get errors at compile time when you forget to change the params at one place thanks to TypeScript.

4. Define the actual component (what should be rendered when navigating to this scene). To get TypeScript autocomplete support for your scene params don't forget to create your component like:
```
export const UserEditScene: React.SFC<
RouteComponentProps<UserEditSceneParams>> = props => {
const { userId, projId } = props.match.params;
return (
...
);
};
```

5. At the bottom you have to create a `UserEditScene` interface with the correct scene params and then an `AppRoute` object called `userEditRoute`. Both have to be exported. It should look like this:
```
export interface UserEditScene extends IScene<UserRoute.Edit> {
params: UserEditSceneParams;
}

export const userEditRoute: AppRoute = {
scene: {
path: UserRoute.Edit,
params: paramsSchema,
},
component: UserEditScene,
};
```
**Important:** The `UserRoute.Edit` will not exist at this time. You will define it in step 6.

**Hint:** The parameter of `IScene` is the path of the scene (Route enum).

**Important:** If a scene doesn't have any params like the [`UserListScene`](https://github.com/npeham/react-typescript-starter/blob/react-router/src/modules/user/scenes/UserList.scene.tsx) you have to define the interface like this:
```
export interface UserListScene extends IScene<UserRoute.List> {
params?: never;
}
```
Also you don't need to create a `paramsSchema` object.

Here is the correctly created [file](https://github.com/npeham/react-typescript-starter/blob/react-router/src/modules/user/scenes/UserEdit.scene.tsx).


6. Open the file [`modules/user/user.routing.ts`](https://github.com/npeham/react-typescript-starter/blob/react-router/src/modules/user/user.routes.ts) - you have to add/change 3 things here.
* At first create a new entry in the `UserRoute` enum:
```
export enum UserRoute {
...
Edit = '/user/edit',
...
}
```
* Add the `userEditRoute` to the `AppRoute` array `userRoutes`:
```
export const userRoutes: AppRoute[] = [userEditRoute, userListScene];
```
* At least add the `UserEditScene` to the `UserScenes` type:
```
export type UserScenes = UserEditScene | UserListScene;
```
**Notice:** The enum doesn't include the params like `'/user/edit/:projId/:userId/'` thanks to the `paramsSchema` object and the [`getRoutePath`](https://github.com/npeham/react-typescript-starter/blob/react-router/src/shared/helper/routing/routing.helper.ts#L7) function.


7. (optional) If you are creating the first scene in your module you also have to do:
- Add the `UserScenes` to the exported `Scenes` type in the [`types/routing.ts`](https://github.com/npeham/react-typescript-starter/blob/react-router/src/shared/types/routing.ts) file.
- Add `mapRoutes(userRoutes)` to the `Switch` tag in the [`App.router.ts`](https://github.com/npeham/react-typescript-starter/blob/react-router/src/App.router.tsx) file.


### Navigate to a scene
If you want to navigate to a scene you have to call the [`getPathWithParams`](https://github.com/npeham/react-typescript-starter/blob/react-router/src/shared/helper/routing/routing.helper.ts#L19) function:
```
const userEditScenePath = getPathWithParams({
path: UserRoute.Edit,
params: {
projId: '3',
userId: '8',
},
});
```
The result is a valid path where you can route to with `react-router`:
```
<button onClick={() => props.history.push(userEditScenePath)}>
click
</button>
```
**Notice:** Thank to our type-safe setup TypeScript will know the params of the scene based on the path (Route enum) you passed to [`getPathWithParams`](https://github.com/npeham/react-typescript-starter/blob/react-router/src/shared/helper/routing/routing.helper.ts#L19) function. So you will get nice autocompletion and you cannot pass the wrong params.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<base href="/" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"react": "^16.4.2",
"react-dom": "^16.4.2",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1"
},
"devDependencies": {
Expand All @@ -21,6 +22,8 @@
"@types/jest": "^24.0.9",
"@types/react": "^16.4.11",
"@types/react-dom": "^16.0.7",
"@types/react-router": "^4.4.4",
"@types/react-router-dom": "^4.3.1",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.10.0",
"enzyme-to-json": "^3.3.5",
Expand Down
30 changes: 30 additions & 0 deletions src/App.router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import { Switch, Route } from 'react-router';
import { AppRoute } from './shared/types/routing';
import { PrivateRoute } from './shared/components/PrivateRoute';
import { userRoutes } from './modules/user/user.routing';
import { postRoutes } from './modules/post/post.routing';
import { verifyIfPathOfSceneIsUnique } from './shared/helper/routing/routing.helper';

const existingPaths: string[] = [];

const mapRoutes = (routeProps: AppRoute[]) => {
return routeProps.map(({ isPrivate, scene, ...rest }) => {
const path = verifyIfPathOfSceneIsUnique(scene, existingPaths);
existingPaths.push(path);
const key = path;

return isPrivate ? (
<PrivateRoute {...{ ...rest, key, path }} />
) : (
<Route {...{ ...rest, key, path }} />
);
});
};

export const AppRouter = () => (
<Switch>
{mapRoutes(userRoutes)}
{mapRoutes(postRoutes)}
</Switch>
);
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react';
import { AppRouter } from './App.router';

export const App = () => <div>working</div>;
export const App = () => <AppRouter />;
8 changes: 7 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';

declare let module: any;

import { App } from './App';

const RenderApp = () => <App />;
const RenderApp = () => (
<BrowserRouter>
<App />
</BrowserRouter>
);

ReactDOM.render(<RenderApp />, document.getElementById('root') as HTMLElement);

Expand Down
10 changes: 10 additions & 0 deletions src/modules/post/post.routing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AppRoute } from '../../shared/types/routing';
import { PostDetailScene, postDetailRoute } from './scenes/PostDetail.scene';

export const enum PostRoute {
Detail = '/post',
}

export const postRoutes: AppRoute[] = [postDetailRoute];

export type PostScenes = PostDetailScene;
46 changes: 46 additions & 0 deletions src/modules/post/scenes/PostDetail.scene.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';
import { RouteComponentProps } from 'react-router';
import { SceneParams, AppRoute, IScene } from '../../../shared/types/routing';
import { UserRoute } from '../../user/user.routing';
import { PostRoute } from '../post.routing';
import { getPathWithParams } from '../../../shared/helper/routing/routing.helper';

export interface PostDetailSceneParams extends SceneParams {
postId: string;
}

const paramsSchema: PostDetailSceneParams = {
postId: '',
};

export const PostDetailScene = (
props: RouteComponentProps<PostDetailSceneParams>,
) => {
const userEditScenePath = getPathWithParams({
path: UserRoute.Edit,
params: {
projId: '3',
userId: '9',
},
});
return (
<React.Fragment>
<div>post detail --> postId: {props.match.params.postId}</div>
<button onClick={() => props.history.push(userEditScenePath)}>
click
</button>
</React.Fragment>
);
};

export interface PostDetailScene extends IScene<PostRoute.Detail> {
params: PostDetailSceneParams;
}

export const postDetailRoute: AppRoute = {
component: PostDetailScene,
scene: {
path: PostRoute.Detail,
params: paramsSchema,
},
};
39 changes: 39 additions & 0 deletions src/modules/user/scenes/UserEdit.scene.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';
import { RouteComponentProps } from 'react-router';
import { SceneParams, AppRoute, IScene } from '../../../shared/types/routing';
import { UserRoute } from '../user.routing';

export interface UserEditSceneParams extends SceneParams {
userId: string;
projId: string;
}

const paramsSchema: UserEditSceneParams = {
projId: '',
userId: '',
};

export const UserEditScene: React.SFC<
RouteComponentProps<UserEditSceneParams>
> = props => {
const { userId, projId } = props.match.params;
return (
<div>
<span>userId --> {userId}</span>
<br />
<span>projId --> {projId}</span>
</div>
);
};

export interface UserEditScene extends IScene<UserRoute.Edit> {
params: UserEditSceneParams;
}

export const userEditRoute: AppRoute = {
scene: {
path: UserRoute.Edit,
params: paramsSchema,
},
component: UserEditScene,
};
19 changes: 19 additions & 0 deletions src/modules/user/scenes/UserList.scene.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';
import { IScene, AppRoute } from '../../../shared/types/routing';
import { UserRoute } from '../user.routing';
import { RouteComponentProps } from 'react-router';

export const UserListScene: React.SFC<RouteComponentProps> = props => {
return <div>user list</div>;
};

export interface UserListScene extends IScene<UserRoute.List> {
params?: never;
}

export const userListScene: AppRoute = {
scene: {
path: UserRoute.List,
},
component: UserListScene,
};
12 changes: 12 additions & 0 deletions src/modules/user/user.routing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { userEditRoute, UserEditScene } from './scenes/UserEdit.scene';
import { UserListScene, userListScene } from './scenes/UserList.scene';
import { AppRoute } from '../../shared/types/routing';

export const enum UserRoute {
List = '/user/list',
Edit = '/user/edit',
}

export const userRoutes: AppRoute[] = [userEditRoute, userListScene];

export type UserScenes = UserEditScene | UserListScene;
8 changes: 8 additions & 0 deletions src/shared/components/PrivateRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as React from 'react';
import { RouteProps, Route } from 'react-router';

type PrivateRoute = RouteProps;

export const PrivateRoute: React.SFC<PrivateRoute> = props => (
<Route {...props} />
);
42 changes: 42 additions & 0 deletions src/shared/helper/object/object.helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { overrideUndefinedValues } from './object.helper';

type TestObject = {
firstValue: string;
secondValue: number;
};

describe('object helper tests', () => {
describe('overrideUndefinedValues', () => {
const defaultObject: TestObject = {
firstValue: 'default',
secondValue: 0,
};

it('returns merged params', () => {
const testObjectWithUndefinedValue: TestObject = {
firstValue: '5',
// @ts-ignore
secondValue: undefined,
};
const mergedParams: TestObject = {
...testObjectWithUndefinedValue,
secondValue: defaultObject.secondValue,
};

expect(
overrideUndefinedValues(testObjectWithUndefinedValue, defaultObject),
).toEqual(mergedParams);
});

it('returns the correct params when no one is undefined', () => {
const testObject: TestObject = {
firstValue: '5',
secondValue: 0,
};

expect(overrideUndefinedValues(testObject, defaultObject)).toEqual(
testObject,
);
});
});
});
18 changes: 18 additions & 0 deletions src/shared/helper/object/object.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type StringSignature = { [key: string]: any };

export function overrideUndefinedValues<T extends StringSignature>(
params: T,
overrideWith: T,
): T {
const paramKeys = Object.keys(params);

const undefinedParamKeys = paramKeys.filter(
(paramKey: string) => params[paramKey] === undefined,
);

const mergedParams = undefinedParamKeys.reduce((prev, undefinedParamKey) => {
return { ...prev, [undefinedParamKey]: overrideWith[undefinedParamKey] };
}, params);

return mergedParams;
}
Loading