Slide 1

Slide 1 text

React at Product Hunt Radoslav Stankov 19/11/2016

Slide 2

Slide 2 text

Radoslav Stankov @rstankov http://rstankov.com http://github.com/rstankov

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Developer Team

Slide 6

Slide 6 text

Tech Stack

Slide 7

Slide 7 text

Tech Stack

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

Tech Stack ES2016 BABEL ESLINT FLOW

Slide 10

Slide 10 text

early 2014 jQuery spaghetti October 2014 Backbone February 2015 React & Rails May 2015 custom Flux

Slide 11

Slide 11 text

December 2015 Redux January 2016 React-Router April 2016 Redux Ducks October 2016 JSTalks talk :)

Slide 12

Slide 12 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 13

Slide 13 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 14

Slide 14 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 15

Slide 15 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 16

Slide 16 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 17

Slide 17 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 18

Slide 18 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 19

Slide 19 text

https://github.com/facebook/flow

Slide 20

Slide 20 text

export type Topic = { id: number, image_uuid?: ?string, name: string, description?: string, followers_count: number, posts_count: number, slug: string, };

Slide 21

Slide 21 text

function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !== -1; }

Slide 22

Slide 22 text

function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !== -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse);

Slide 23

Slide 23 text

function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !== -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse);

Slide 24

Slide 24 text

function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !== -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse); const variable: number = isFollowing(user, topic);

Slide 25

Slide 25 text

function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !== -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse); const variable: number = isFollowing(user, topic);

Slide 26

Slide 26 text

function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !== -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse); const variable: number = isFollowing(user, topic); const variable: boolean = isFollowing(user, topic);

Slide 27

Slide 27 text

function isFollowing( user: { followedTopicsIds: Array }, topic: { id: number } ): boolean { return user.followedTopicsIds.indexOf(topic.id) !== -1; }

Slide 28

Slide 28 text

Slide 29

Slide 29 text

Slide 30

Slide 30 text

Slide 31

Slide 31 text

class UserImage extends React.Component { props: {| user: User, width: number, height: number |} | {| user: User, variant: 'big' | 'medium' | 'small' |}; render() { /* ... */ } }

Slide 32

Slide 32 text

https://github.com/facebook/flow

Slide 33

Slide 33 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 34

Slide 34 text

https://github.com/producthunt/duxy

Slide 35

Slide 35 text

const http = createHttpAdapter(); export default duxy({ http }, ({ resources, resource, get }) => { get('context'); resources('topics', { only: ['findAll', 'findOne'] }, () => { resource('topContributors', { path: 'top_contributors', only: ['findAll'] }); resource('followers', { only: ['create', 'delete'] }); }); // ... more definitions });

Slide 36

Slide 36 text

import api from 'ph/api'; const response = await api.context(); // GET /context const response = await api.topics.findOne({ id: 1 }) // GET /topics/1 const response = await api.topics.findAll({ limit: 10 }) // GET /topics?limit=10

Slide 37

Slide 37 text

import api from 'ph/api';
 
 try { // POST /topics/1/followers const response = await api.topics.followers.create({ topicId: 1 }); } catch(e) { // handle errors }

Slide 38

Slide 38 text

⚽ resources - findAll / findOne / create / update / delete resource - findAll / create / update / delete get ⚾ post patch delete namespace adapters Duxy

Slide 39

Slide 39 text

https://github.com/producthunt/duxy

Slide 40

Slide 40 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

Slide 45

Slide 45 text

Slide 46

Slide 46 text

Slide 47

Slide 47 text

Slide 48

Slide 48 text

Slide 49

Slide 49 text

Slide 50

Slide 50 text

Slide 51

Slide 51 text

function TopicItemList({ topics }: { topics: Array }) { return (
    {topics.map((topic) => (
  1. ))}
); }

Slide 52

Slide 52 text

CSS // style.css .list { list-style: none; } CSS // component.js
 import styles from './styles.css';
CSS // style.css .list_3mRwv { list-style: none; } CSS // result.js

Slide 53

Slide 53 text

function TopicItemList({ topics }: { topics: Array }) { return (
    {topics.map((topic) => (
  1. ))}
); }

Slide 54

Slide 54 text

describe(TopicItemList.name, () => { it('renders list of topics', () => { const topic = new TopicFactory(); const wrapper = shallow() expect(wrapper).to.contain(

Slide 55

Slide 55 text

https://github.com/producthunt/chai-enzyme

Slide 56

Slide 56 text

No content

Slide 57

Slide 57 text

Slide 58

Slide 58 text

Slide 59

Slide 59 text

Slide 60

Slide 60 text

Slide 61

Slide 61 text

Slide 62

Slide 62 text

Slide 63

Slide 63 text

Slide 64

Slide 64 text

function TopicItem({ topic }: { topic: Topic }) { return (
{topic.name} {followersCount(topic.followers_count)}
); }

Slide 65

Slide 65 text

… … … … … … …

Slide 66

Slide 66 text

function TopicItem({ topic }: { topic: Topic }) { return (
{topic.name} {followersCount(topic.followers_count)}
); }

Slide 67

Slide 67 text

export class TopicFollowButton extends React.Component { props: { isFollowing: boolean, toggle: typeof toggleTopicFollow, topicId: number, className: string, }; render() { return ( ); } handleClickAction: Function = () => {

Slide 68

Slide 68 text

export createContainer({ renderComponent: Component, actionCreators: actionCreators, mapStateToProps: mapStateToProps, }); export connect(mapStateToProps, actionCreators)(Component)

Slide 69

Slide 69 text

export createContainer({ renderComponent: Component, clientFetch() { // Browser data load }, });
 
 class Component extends React.Component { componentDidMount() { clientFetch(); } }

Slide 70

Slide 70 text

export createContainer({ renderComponent: Component, clientFetch(dispatch, getState, api): Promise { // Browser data load }, fetch(dispatch, getState, api): Promise { // Browser/Server data load }, });

Slide 71

Slide 71 text

export createContainer({ renderComponent: Component, decorators: [ renderNotFound(someCondition), renderLoading(), addBodyClassName('white-background') documentTitle('About page'), requireEnabledFeature('new_topics') // ... ] });

Slide 72

Slide 72 text

export class TopicFollowButton extends React.Component { props: { isFollowing: boolean, toggle: typeof toggleTopicFollow, topicId: number, className: string, }; render() { return ( ); } handleClickAction: Function = () => {

Slide 73

Slide 73 text

describe(TopicFollowButton.name, () => { const call = sut(TopicFollowButton, { isFollowing: true, topicId: 1, toggle() {}, }); it('renders "Follow" text when is not following', () => { const wrapper = shallow(call({ isFollowing: false })); expect(wrapper).to.contain('Follow'); }); it('renders "Following" text when is following', () => { const wrapper = shallow(call({ isFollowing: true })); expect(wrapper).to.contain('Following'); }); it('can follow a topic', () => { const spy = sinon.spy(); const wrapper = shallow(call({ toggle: spy, topicId: 1 }));

Slide 74

Slide 74 text

/components/Box/index.js /components/Button/index.js /components/Fonts/index.js /components/InfiniteScroll/index.js /components/TopicFollowButton/index.js /components/TopicItemList/Item/index.js /components/TopicItemList/index.js

Slide 75

Slide 75 text

No content

Slide 76

Slide 76 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 77

Slide 77 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 78

Slide 78 text

Ducks: Redux Reducer Bundles https://github.com/erikras/ducks-modular-redux

Slide 79

Slide 79 text

/modules/annotations.js /modules/collections.js /modules/context.js /modules/currentUser.js /modules/pages.js /modules/posts.js /modules/topics.js

Slide 80

Slide 80 text

modules/[reducer].js 1. Actions 2. Reducer 3. Action Creators 4. Selectors 5. Other stuff ¯\_(ツ)_/¯

Slide 81

Slide 81 text

modules/[reducer].js 1. Actions 2. Reducer 3. Action Creators 4. Selectors 5. Other stuff ¯\_(ツ)_/¯

Slide 82

Slide 82 text

// Actions export const FOLLOW_TOPIC = 'TOPICS/FOLLOW'; export const RECEIVE_TOPICS = 'TOPICS/RECEIVE'; export const UNFOLLOW_TOPIC = 'TOPICS/UNFOLLOW'; // Reducer type TopicsState = Immutable.Map; export const initialState: TopicsState = new Immutable.Map(); export default createReducer(initialState, { [RECEIVE_TOPICS]: (state: TopicsState, action: Action): TopicsState => { /* ... */ }, [FOLLOW_TOPIC]: (state: TopicsState, action: Action): TopicsState => { /* ... */ }, [UNFOLLOW_TOPIC]: (state: TopicsState, action: Action): TopicsState => { /*...*/ }, }); // Action Creators export function receiveTopic({ topic }: { topic: Topic }): Dispatchable { return { type: RECEIVE_TOPICS, payload: { topics: [topic] } }; } export function receiveTopics({ topics }: { topics: Array }): Dispatchable { return { type: RECEIVE_TOPICS, payload: { topics } }; } export function toggleTopicFollow(topicId: number): Dispatchable { return function(dispatch: Dispatch, getState: Function, api: any): void { const { currentUser } = getState(); if (isFollowingTopic(currentUser, id)) {

Slide 83

Slide 83 text

export function toggleTopicFollow(topicId: number): Dispatchable { return function(dispatch: Dispatch, getState: Function, api: any): void { const { currentUser } = getState(); if (isFollowingTopic(currentUser, id)) { api.topics.followers.delete({ topicId }); dispatch({ type: UNFOLLOW_TOPIC, payload: { topicId } }); } else { api.topics.followers.create({ topicId }); dispatch({ type: FOLLOW_TOPIC, payload: { topicId } }); } }; }

Slide 84

Slide 84 text

https://github.com/gaearon/redux-thunk

Slide 85

Slide 85 text

function action() { return { type: ACTION,
 payload: {}, }; } redux-thunk

Slide 86

Slide 86 text

function action() { return (dispatch, getState) => { // do stuff dispatch({ type: ACTION_1 }); // do stuff dispatch({ type: ACTION_2 }); // ...so on }; } redux-thunk

Slide 87

Slide 87 text

import { applyMiddleware, compose, createStore } from 'redux'; import api from 'ph/api'; import reducer from 'ph/reducers'; export default function createStore(state = {}, options = {}) { const enhancers = [ applyMiddleware(thunkInject(options.api || api)), // ... other middlewares ]; return compose(...enhancers)(createStore)(reducer, initialState); }

Slide 88

Slide 88 text

export function toggleTopicFollow(topicId: number): Dispatchable { return function(dispatch: Dispatch, getState: Function, api: any): void { const { currentUser } = getState(); if (isFollowingTopic(currentUser, topicId)) { api.topics.followers.delete({ topicId }); dispatch({ type: UNFOLLOW_TOPIC, payload: { topicId } }); } else { api.topics.followers.create({ topicId }); dispatch({ type: FOLLOW_TOPIC, payload: { topicId } }); } }; }

Slide 89

Slide 89 text

describe(toggleTopicFollow.name, () => { const api = stubApi({ 'api.frontend.topics.followers.create': {}, 'api.frontend.topics.followers.delete': {}, }); const testStore = ({ topic, currentUser }) => { const store = createStore({}, { api }); store.dispatch(receiveCurrentUser(currentUser)); store.dispatch(receiveTopic(topic)); return store; } const getState = (store) => store.getState().topics; it('increases followers count on follow', () => { const topic = new TopicFactory({ followersCount: 0 }); const currentUser = new CurrentUserFactory(); const store = testStore({ topic, currentUser }); store.dispatch(toggleTopicFollow(topic)); expect(topicById(getState(store), topic.id).followersCount).to.equal(1); }); it('decreases followers count on unfollow', () => { const topic = new TopicFactory({ followersCount: 1 }); const currentUser = new CurrentUserFactory({ followedTopicIds: [topic.id] });

Slide 90

Slide 90 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 91

Slide 91 text

https://github.com/ReactTraining/react-router v3

Slide 92

Slide 92 text

// routes.js export default createRoutes( );

Slide 93

Slide 93 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 94

Slide 94 text

// paths.js export default { root(): string { return '/'; }, pages: { about(): string { return '/about'; }, // ... } topics: { index(): string { return '/topics'; }, show({ slug }: Topic): string { return `/topics/${ slug }`; }, }, // ... };

Slide 95

Slide 95 text

import paths from 'ph/paths';
 
 paths.pages.about(); // => /about/ paths.topics.index(); // => /topics/ paths.topics.show(topic); // => /topics/developer-tools

Slide 96

Slide 96 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 97

Slide 97 text

// routes.js export default createRoutes( );

Slide 98

Slide 98 text

export default function Main({ children }: { children: any }) { return (
{children}
); }

Slide 99

Slide 99 text

No content

Slide 100

Slide 100 text

No content

Slide 101

Slide 101 text

{children}

Slide 102

Slide 102 text

{children}

Slide 103

Slide 103 text

{children}

Slide 104

Slide 104 text

{children}

Slide 105

Slide 105 text

{children}

Slide 106

Slide 106 text

{children}

Slide 107

Slide 107 text

{children}

Slide 108

Slide 108 text

export function Profile({ children, user, components, topics }: Pro return (
{children}
); } export default createContainer({ renderComponent: Profile, decorators: [ renderLoading(), renderNotFound((user) => !!user), ], fetch({ dispatch, getState, params: { username } }) { return dispatch(fetchProfile(username));

Slide 109

Slide 109 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 110

Slide 110 text

No content

Slide 111

Slide 111 text

{ isLoading isLoaded resourceId/resourceIds ...otherValues }

Slide 112

Slide 112 text

{ isLoading isLoaded postId postSlug }

Slide 113

Slide 113 text

LOADED RESET LOADING UPDATE

Slide 114

Slide 114 text

// Actions export const RESET = 'PAGES/RESET'; export const LOADING = 'PAGES/LOADING'; export const LOADED = 'PAGES/LOADED'; export const UPDATE = 'PAGES/UPDATE'; // Pages export const POST = 'PAGES/POST'; const PAGES = { [POST]: { isLoading: false, isLoaded: false, postId: [], postSlug: null, }, // ... other pages }; // ... other pages const initialState = new Immutable.Map(PAGES);

Slide 115

Slide 115 text

export default createContainer({ renderComponent: Post, decorators: [ renderLoading(), documentTitle(({ post }) => post.title), ], fetch({ dispatch, params: { slug } }) { return dispatch(fetchPostPage(slug)); }, mapStateToProps({ pages, posts }) { const page = getPage(page, POST); return { ...page, post: postById(posts, page.postId), }; }, });

Slide 116

Slide 116 text

export function fetchPostPage(slug: string): Dispatchable { return async (dispatch, getState, api) => { const page = getPage(getState().pages, POST); if (page.postSlug === slug) { return; } dispatch(reset(POST, { isLoading: true, postSlug: slug })); const { body } = await api.frontend.posts.findOne({id: slug}); dispatch(bootstrap(body)); dispatch(loaded(POST, { postId: body.post.id, })); }; }

Slide 117

Slide 117 text

components/ layouts/ lib/ modules/ pages/ styles/ types/ utils/ api.js config.js entry.js paths.js reducers.js routes.js

Slide 118

Slide 118 text

No content

Slide 119

Slide 119 text

https://github.com/ReactTraining/react-router https://github.com/airbnb/enzyme/ https://github.com/chaijs/chai https://github.com/css-modules/css-modules https://github.com/erikras/ducks-modular-redux https://github.com/eslint/eslint https://github.com/facebook/flow https://github.com/gaearon/redux-thunk https://github.com/jnunemaker/flipper https://github.com/producthunt/chai-enzyme https://github.com/producthunt/duxy

Slide 120

Slide 120 text

https://speakerdeck.com/rstankov/react-at-product-hunt

Slide 121

Slide 121 text

https://www.meetup.com/React-Sofia/

Slide 122

Slide 122 text

Thanks

Slide 123

Slide 123 text

No content