Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ReactJS- With Types and Hooks

vipulnsward
February 29, 2020

ReactJS- With Types and Hooks

The presentation shares a story on how Gumroad is migrating from FlightJS to ReactJS making use of Hooks and Typescript.

vipulnsward

February 29, 2020
Tweet

More Decks by vipulnsward

Other Decks in Technology

Transcript

  1. - Big Churn -Large codebases are confusing - Changes at

    a lot of places - Harder to make change
  2. "use strict"; import React from "react"; import { useOnChange }

    from "$app/components/useOnChange"; import { CustomFieldRow } from "./row"; const ADD_FIELD = "add-field"; const REMOVE_FIELD = "remove-field"; const CHANGE_NAME = "change-name"; const CHANGE_IS_REQUIRED = "change-is-required"; const RESET = "reset"; const customFieldsReducer = (state, action) => { switch (action.type) { case ADD_FIELD: { return [...state, { name: "", required: false }]; } case REMOVE_FIELD: { return state.filter((_, idx) => idx !== action.index); } case CHANGE_NAME: { return state.map((fld, idx) => { if (idx === action.index) { return { ...fld, name: action.newName }; } return fld; }); } case CHANGE_IS_REQUIRED: { return state.map((fld, idx) => { if (idx === action.index) { return { ...fld, required: action.newValue }; } return fld; }); } case RESET: { return action.customFields; } default: return state; } }; const CustomFields = ({ customFields, onStateChange }) => { const [customFieldsState, dispatch] = React.useReducer( customFieldsReducer, customFields, customFields => customFields.length === 0 ? [{ name: "", required: false }] : customFields ); const isInitial = React.useRef(true); React.useEffect(() => { onStateChange( customFieldsState.filter(field => field.name.length > 0), isInitial.current ); isInitial.current = false; }, [customFieldsState]); useOnChange(() => { isInitial.current = true; dispatch({ type: RESET, customFields }); }, [customFields]); return ( <React.Fragment> {customFieldsState.map((field, idx) => ( <CustomFieldRow key={idx} name={field.name} isRequired={field.required} onChangeName={newName => dispatch({ type: CHANGE_NAME, index: idx, newName }) } onChangeIsRequired={newValue => dispatch({ type: CHANGE_IS_REQUIRED, index: idx, newValue }) } onRemove={() => dispatch({ type: REMOVE_FIELD, index: idx })} /> ))} <button type="button" className="button-small button-default" onClick={() => dispatch({ type: ADD_FIELD })} > {I18n.t("js.add_custom_field_button_label")} </button> </React.Fragment> ); }; export { CustomFields };
  3. const CustomFields = ({ customFields, onStateChange }) => { const

    [customFieldsState, dispatch] = React.useReducer( customFieldsReducer, customFields, customFields => customFields.length === 0 ? [{ name: "", required: false }] : customFields ); const isInitial = React.useRef(true); React.useEffect(() => { onStateChange( customFieldsState.filter(field => field.name.length > 0), isInitial.current ); isInitial.current = false; }, [customFieldsState]); useOnChange(() => { isInitial.current = true; dispatch({ type: RESET, customFields }); }, [customFields]); };
  4. <React.Fragment> {customFieldsState.map((field, idx) => ( <CustomFieldRow key={idx} name={field.name} isRequired={field.required} onChangeName={newName

    => dispatch({ type: CHANGE_NAME, index: idx, newName }) } onChangeIsRequired={newValue => dispatch({ type: CHANGE_IS_REQUIRED, index: idx, newValue }) } onRemove={() => dispatch({ type: REMOVE_FIELD, index: idx })} /> ))} <button type="button" className="button-small button-default" onClick={() => dispatch({ type: ADD_FIELD })} > {I18n.t("js.add_custom_field_button_label")} </button> </React.Fragment>
  5. const ADD_FIELD = "add-field"; const REMOVE_FIELD = "remove-field"; const CHANGE_NAME

    = "change-name"; const CHANGE_IS_REQUIRED = "change-is-required"; const RESET = "reset"; const customFieldsReducer = (state, action) => { switch (action.type) { case ADD_FIELD: { .... } case REMOVE_FIELD: { .... } case CHANGE_NAME: { ... } case CHANGE_IS_REQUIRED: { .... } case RESET: { ... } default: return state; } };
  6. this.renderCustomFieldsComponent = function(customFields) { const customFieldsRoot = document.getElementById("custom_fields_root"); // There

    isn't much that can go wrong here, but in case something does, // let's log the error and show a message to the user in place of the component. // Error boundary is like a "try-catch" for components. ReactDOM.render( <ErrorBoundary placeholder={<p>{I18n.t("js.error_placeholder")}</p>}> <CustomFields customFields={customFields} onStateChange={(newState, isInitial) => { this.attr.customFieldsState = newState; if (!isInitial) { this.updateCustomFieldsInputs(); } this.trigger("uiUpdateCurrentProductPageTabHeight"); }} /> </ErrorBoundary>, customFieldsRoot ); };
  7. // Behaves like `useEffect`, except isn't not called on the

    first render. export function useOnChange(cb, deps) { const isFirstRender = React.useRef(true); React.useEffect(() => { if (isFirstRender.current === false) { cb(); } else { isFirstRender.current = true; } }, deps); }
  8. class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state =

    { hasError: false }; } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error, errorInfo) { console.error(error); } render() { if (this.state.hasError) { return this.props.placeholder; } return this.props.children; } } export { ErrorBoundary };
  9. const ADD_FIELD = "add-field"; const REMOVE_FIELD = "remove-field"; const CHANGE_NAME

    = "change-name"; const CHANGE_IS_REQUIRED = "change-is-required"; const RESET = "reset"; const customFieldsReducer = (state, action) => { switch (action.type) { case ADD_FIELD: { .... } case REMOVE_FIELD: { .... } case CHANGE_NAME: { ... } case CHANGE_IS_REQUIRED: { .... } case RESET: { ... } default: return state; } };
  10. type CustomField = { name: string; required: boolean }; type

    State = Array<CustomField>; type Action = | { type: "add-field" } | { type: "remove-field"; index: number } | { type: "change-name"; index: number; newName: string } | { type: "change-is-required"; index: number; newValue: boolean } | { type: "reset"; customFields: Array<CustomField> }; const customFieldsReducer = (state: State, action: Action): State => { switch (action.type) { case "add-field": { .. } case "remove-field": { ... } case "change-name": { ... } case "change-is-required": { ... } case "reset": { ... } default: return state; } };
  11. type FilesAction = | { type: "start-file-upload"; fileEntry: FileEntry }

    | { type: "update-file-upload-progress"; fileId: string; progress: UploadProgress; } | { type: "finish-file-upload"; fileId: string } | { type: "add-existing-file"; fileEntry: FileEntry } | { type: "upsert-dropbox-files"; fileEntries: Array<FileEntry> } | { type: "remove-file"; fileId: string } | { type: "rename-file"; fileId: string; newName: string } | { type: "cancel-file-upload"; fileId: string } | { type: "upload-subtitle-files"; fileId: string; subtitleFiles: Array<DOMFile>;} | { type: "start-subtitle-upload"; fileId: string; subtitleEntry: SubtitleFile; } | { type: "update-subtitle-upload-progress"; fileId: string; subtitleUrl: string; progress: UploadProgress; } | { type: "finish-subtitle-upload"; fileId: string; subtitleUrl: string } | { type: "cancel-subtitle-upload"; fileId: string; subtitleUrl: string } | { type: "remove-subtitle"; fileId: string; subtitleUrl: string } ...