Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React generic components #7

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 95 additions & 0 deletions 2018-05/ts-react-generic-components/blogpost.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# React Generic Components with TypeScript

TypeScript 2.9 introduced a new compiler feature for declaring generic React components from within JSX, which adds even more type-safety and developer experience.

What do I mean by React Generic Component ?

> we will use the same Select component for all examples in this post
>
> Select is a custom input element that leverages `datalist` HTML element

**Class Component:**

```tsx
import React, { Component, SyntheticEvent } from 'react'

type Props<T> = {
active: T
items: T[]
onSelect(item: T, event?: SyntheticEvent<HTMLElement>): void
}
class Select<T> extends Component<Props<T>> {
render() {
return <div>{...}</div>
}
}
```

**Function Component:**

```tsx
import React, { SyntheticEvent } from 'react'

type Props<T> = {
active: T
items: T[]
onSelect(item: T, event?: SyntheticEvent<HTMLElement>): void
}

const Select = <T extends {}>(props: Props<T>) => { return <div>{...}</div> }
```

With that said, you may be still asking, ok dude but why do I need to use generics for my re-usable `Select` ?

Well, re-usable === it should accept and render various data types:

- primitive ones `string[]`
- more complicated data like objects/arrays etc...

How should it be used in type-safe way?

We have some App component with static data and state:

```tsx
const data = {
heroes: ['Hulk', 'Iron Man'],
users: [{ name: 'Peter', age: 32 }, { name: 'John', age: 23 }],
}

class App extends Component {
state = {
hero: '',
user: null,
}
render() {
return (
<>
<Select
active={this.hero}
items={data.heroes}
onSelect={(selected) => this.setState((prevState) => ({ hero: selected }))}
/>
<Select
active={this.user}
items={data.users}
onSelect={(selected) => this.setState((prevState) => ({ user: selected }))}
/>
</>
)
}
}
```

Let's implement our generic **<Select />** !

### Generic <Select/>

#### 1. Defining Props

#### 2. Defining State

#### 3. Implementation

### Generic <Select/> with render prop pattern

### Summary
23 changes: 23 additions & 0 deletions 2018-05/ts-react-generic-components/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "ts-react-generic-components",
"version": "1.0.0",
"main": "index.js",
"author": "Martin Hochel <[email protected]>",
"license": "MIT",
"private": true,
"scripts": {
"start": "parcel ./src/index.html",
"format": "prettier --write \"**/*.{ts,tsx,md,css,less,sass,scss}\""
},
"devDependencies": {
"@types/react": "16.4.6",
"@types/react-dom": "16.0.6",
"parcel-bundler": "1.9.4",
"prettier": "1.13.7",
"typescript": "2.9.2"
},
"dependencies": {
"react": "16.4.1",
"react-dom": "16.4.1"
}
}
104 changes: 104 additions & 0 deletions 2018-05/ts-react-generic-components/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { Component } from 'react'

import { Select, SelectSFC } from './components/select'
import { AppDemo } from './components/select-blogpost-demo'

import './style.css'

interface User {
uname: string
age: number
}
const isUser = (value: any): value is User =>
typeof value === 'object' && (value.uname || value.age)

type State = typeof initialState
const initialState = {
selected: {},
collections: {
iceCream: ['Chocolate', 'Coconut', 'Mint', 'Strawberry', 'Vanilla'],
browsers: ['Chrome', 'Firefox', 'Internet Explorer', 'Opera', 'Safari', 'Microsoft Edge'],
library: ['React', 'Preact', 'Vue', 'jQuery'],
users: [
{ uname: 'Martin', age: 31 },
{ uname: 'Peter', age: 32 },
{ uname: 'Anna', age: 26 },
] as User[],
players: [
{ uname: 'Sean', age: 22 },
{ uname: 'Carl', age: 12 },
{ uname: 'Maria', age: 24 },
] as User[],
},
}
export class App extends Component<{}, State> {
static SelectedItems = (props: { selected: { [key: string]: any } }) => {
const keys = Object.keys(props.selected)
const renderItem = (key: string) => {
const value = props.selected[key]
const renderElem = isUser(value) ? value.uname : value
return <li key={key}>{renderElem}</li>
}

return <ul>{keys.map(renderItem)}</ul>
}
static Debug = (props: { value: {} }) => <pre>{JSON.stringify(props.value, null, 2)}</pre>
state = initialState
private handleSelect = (itemValue: string | User, ev: React.SyntheticEvent<HTMLInputElement>) => {
const { name: fieldName } = ev.currentTarget
this.setState((prevState) => ({
...prevState,
selected: { ...prevState.selected, [fieldName]: itemValue },
}))
}
render() {
return (
<main>
<h1>Generic Components</h1>
<section className="container">
<AppDemo />
</section>
{/* <hr />
<section className="container">
<div>
Selected items:
<App.SelectedItems selected={this.state.selected} />
<App.Debug value={this.state.selected} />
</div>
<div>
<Select
name="ice-cream-choice"
label="Choose a flavor"
items={this.state.collections.iceCream}
onSelect={this.handleSelect}
/>
<Select<string>
name="my-browser"
label="Choose a browser from this list"
items={this.state.collections.browsers}
onSelect={this.handleSelect}
/>
<Select<User>
name="my-user"
label="Choose user from this list"
items={this.state.collections.users}
onSelect={this.handleSelect}
/>
<SelectSFC<string>
name="my-library"
label="Choose your favourite library"
items={this.state.collections.library}
onSelect={this.handleSelect}
/>
<SelectSFC<User>
name="my-character"
label="Choose a character"
items={this.state.collections.players}
onSelect={this.handleSelect}
/>
</div>
</section> */}
</main>
)
}
}
9 changes: 9 additions & 0 deletions 2018-05/ts-react-generic-components/src/components/debug.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { SFC, isValidElement } from 'react'

export const Debug: SFC = (props) => {
if (isValidElement(props.children)) {
throw new Error('ReactElement is not allowed!')
}

return <pre>{JSON.stringify(props.children)}</pre>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { Component } from 'react'
import { Select } from './select-blogpost'
import { Select as SelectRender } from './select-with-render-props'
import { Debug } from './debug'

type User = { name: string; age: number }

type Props = {}
type State = typeof initialState

const data = {
heroes: ['Hulk', 'Iron Man'],
users: [{ name: 'Peter', age: 32 }, { name: 'John', age: 23 }] as User[],
}

const initialState = {
hero: '' as string | null,
user: null as User | null,
}

export class AppDemo extends Component<Props, State> {
static ListItem = (props: { value: any }) => <option value={props.value} />
state = initialState
render() {
return (
<>
<Debug>{this.state}</Debug>
<hr />
<section>
<h3>1. Generic Select</h3>
<Select<string>
key={String(this.state.hero)}
name="hero"
label="selec hero"
active={this.state.hero}
items={data.heroes}
onSelect={(selected) => this.setState((prevState) => ({ hero: selected }))}
/>
<Select<User>
key={this.state.user ? this.state.user.name : undefined}
name="user"
label="selec user"
displayKey="name"
active={this.state.user}
items={data.users}
onSelect={(selected) => this.setState((prevState) => ({ user: selected }))}
/>
</section>

<hr />

<section>
<h3>2. Generic Select with Render Prop</h3>
<SelectRender<string>
key={String(this.state.hero)}
name="hero"
label="selec hero"
active={this.state.hero}
items={data.heroes}
onSelect={(selected) => this.setState(() => ({ hero: selected }))}
>
{(item) => <option value={item} />}
</SelectRender>
<SelectRender<User>
key={this.state.user ? this.state.user.name : undefined}
name="user"
label="selec user"
displayKey="name"
active={this.state.user}
items={data.users}
onSelect={(selected) => this.setState(() => ({ user: selected }))}
>
{(item) => <AppDemo.ListItem value={item.age} />}
</SelectRender>
</section>
</>
)
}
}
Loading