Skip to content

Instantly share code, notes, and snippets.

@jamesarosen
Last active June 8, 2023 16:45
Show Gist options
  • Select an option

  • Save jamesarosen/d380b6a73da0d695d1f7df01b95d02f7 to your computer and use it in GitHub Desktop.

Select an option

Save jamesarosen/d380b6a73da0d695d1f7df01b95d02f7 to your computer and use it in GitHub Desktop.

Revisions

  1. jamesarosen revised this gist Nov 23, 2019. 1 changed file with 4 additions and 5 deletions.
    9 changes: 4 additions & 5 deletions helper-functions-in-redux.md
    Original file line number Diff line number Diff line change
    @@ -84,18 +84,17 @@ For applications that _do_ use Redux, we can partially apply the function in a r
    // reducers/helpers.js
    import imageUrlForRoot from 'lib/image-url'

    export function helpers(state = {}, action) {
    const defaultState = { imageUrl: imageUrlForRoot(null) }

    export function helpers(state = defaultState, action) {
    switch (action.type) {
    case 'RECEIVE_CONSTANTS':
    return {
    ...state,
    imageUrl: imageUrlForRoot(action.payload.imagesRoot),
    }
    default:
    return {
    ...state,
    imageUrl: state.imageUrl || imageUrlForRoot(null),
    }
    return state
    }
    }
    ```
  2. jamesarosen revised this gist Nov 23, 2019. 1 changed file with 5 additions and 2 deletions.
    7 changes: 5 additions & 2 deletions helper-functions-in-redux.md
    Original file line number Diff line number Diff line change
    @@ -89,10 +89,13 @@ export function helpers(state = {}, action) {
    case 'RECEIVE_CONSTANTS':
    return {
    ...state,
    imageUrl: imageUrlForRoot(action.payload.imagesRoot)
    imageUrl: imageUrlForRoot(action.payload.imagesRoot),
    }
    default:
    return state
    return {
    ...state,
    imageUrl: state.imageUrl || imageUrlForRoot(null),
    }
    }
    }
    ```
  3. jamesarosen revised this gist Nov 23, 2019. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions helper-functions-in-redux.md
    Original file line number Diff line number Diff line change
    @@ -115,5 +115,6 @@ export default function Image({ imageId, alt, size }) {
    - **Con**: Requires the JS function plus an action-creator and a reducer
    - **Con**: Can't look at the `import` statements at the top of the file to find function dependencies
    - **Con**: Not a known pattern.
    - **Con**: Temporal coupling in the Redux version. If some root component doesn't dispatch an action that causes `RECEIVE_CONSTANTS`, all uses of `imageUrl` will return `null`.

    Have you done this? Do you like the idea of putting partially-applied functions into the Redux store? Do you have a better alternative?
  4. jamesarosen revised this gist Nov 23, 2019. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions helper-functions-in-redux.md
    Original file line number Diff line number Diff line change
    @@ -113,6 +113,7 @@ export default function Image({ imageId, alt, size }) {
    - **Pro**: Very easy to test
    - **Pro**: Usable with or without Redux
    - **Con**: Requires the JS function plus an action-creator and a reducer
    - **Con**: Can't look at the `import` statements at the top of the file to find function dependencies
    - **Con**: Not a known pattern.

    Have you done this? Do you like the idea of putting partially-applied functions into the Redux store? Do you have a better alternative?
  5. jamesarosen revised this gist Nov 23, 2019. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions helper-functions-in-redux.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    I have a function that generates image URLs. This function combines some relatively static global configuration with some dynamic data that changes on every invocation. I say "relatively static" because the configuration is loaded asynchronously during the application boot, but remains fixed after that.

    h3. Option One
    ### Option One

    ```js
    export default async function imageUrl(imageId, { size = 'normal' }) {
    @@ -18,7 +18,7 @@ export default async function imageUrl(imageId, { size = 'normal' }) {
    - **Con**: If `GET /api/constants` isn't cached in the browser, it's terrible for rendering performance.
    - **Con**: Testing image URL generation requires stubbing an HTTP API.

    h3. Option Two
    ### Option Two

    An alternative would be to take the root as an argument:

    @@ -36,7 +36,7 @@ export default function imageUrl(imageId, { size = 'normal', imagesRoot }) {
    - **Con**: requires passing the `imagesRoot` around all over the application.
    - **Con**: May end up with a performance problem if multiple components do the `fetch('/api/constants')` call and don't cache the result.

    h3. Option Three
    ### Option Three

    We could have our JavaScript function reach out to some global state:

    @@ -60,7 +60,7 @@ export default function imageUrl(imageId, { size = 'normal' }) {
    - **Con**: Tied to Redux. In fact, it's tied to an instance of the Redux store being available at `my/redux/store`
    - **Con**: Temporal coupling. If you call the function before dispatching the `fetchConstants` action that populates `state.constants.imagesRoot`, you get `null` back.

    h3. Option Four
    ### Option Four

    We can keep the function _pure_ by using Currying:

  6. jamesarosen revised this gist Nov 23, 2019. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions helper-functions-in-redux.md
    Original file line number Diff line number Diff line change
    @@ -81,10 +81,10 @@ imgTag.src = imageUrlForRoot('https://example.com/images')('cheese.png')
    For applications that _do_ use Redux, we can partially apply the function in a reducer:

    ```js
    // reducers/constants.js
    // reducers/helpers.js
    import imageUrlForRoot from 'lib/image-url'

    export function constants(state = {}, action) {
    export function helpers(state = {}, action) {
    switch (action.type) {
    case 'RECEIVE_CONSTANTS':
    return {
    @@ -102,7 +102,7 @@ And consume it in a component:
    import { useSelector } from 'react-redux'

    export default function Image({ imageId, alt, size }) {
    const imageUrl = useSelector('constants.imageUrl')
    const imageUrl = useSelector('helpers.imageUrl')
    const src = imageUrl(imageId, { size })
    return <img alt={alt} src={src} />
    }
  7. jamesarosen revised this gist Nov 23, 2019. 1 changed file with 7 additions and 0 deletions.
    7 changes: 7 additions & 0 deletions helper-functions-in-redux.md
    Original file line number Diff line number Diff line change
    @@ -108,4 +108,11 @@ export default function Image({ imageId, alt, size }) {
    }
    ```

    - **Pro**: Fast
    - **Pro**: No `async`
    - **Pro**: Very easy to test
    - **Pro**: Usable with or without Redux
    - **Con**: Requires the JS function plus an action-creator and a reducer
    - **Con**: Not a known pattern.

    Have you done this? Do you like the idea of putting partially-applied functions into the Redux store? Do you have a better alternative?
  8. jamesarosen created this gist Nov 23, 2019.
    111 changes: 111 additions & 0 deletions helper-functions-in-redux.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,111 @@
    I have a function that generates image URLs. This function combines some relatively static global configuration with some dynamic data that changes on every invocation. I say "relatively static" because the configuration is loaded asynchronously during the application boot, but remains fixed after that.

    h3. Option One

    ```js
    export default async function imageUrl(imageId, { size = 'normal' }) {
    if (imageId == null) return null

    const constantsResponse = await fetch('/api/constants')
    const imagesRoot = constantsResponse.json().imagesRoot

    return `${imagesRoot}/${imageId}?size=${size}`
    }
    ```

    - **Pro**: This has great cohesion -- everything required for generating image URLs is all expressed in one place.
    - **Con**: The `async` nature makes this hard to use in components.
    - **Con**: If `GET /api/constants` isn't cached in the browser, it's terrible for rendering performance.
    - **Con**: Testing image URL generation requires stubbing an HTTP API.

    h3. Option Two

    An alternative would be to take the root as an argument:

    ```js
    export default function imageUrl(imageId, { size = 'normal', imagesRoot }) {
    if (imageId == null || imagesRoot == null) return null

    return `${imagesRoot}/${imageId}?size=${size}`
    }
    ```

    - **Pro**: Fast
    - **Pro**: No `async`
    - **Pro**: Easy to test
    - **Con**: requires passing the `imagesRoot` around all over the application.
    - **Con**: May end up with a performance problem if multiple components do the `fetch('/api/constants')` call and don't cache the result.

    h3. Option Three

    We could have our JavaScript function reach out to some global state:

    ```js
    import store from 'my/redux/store'

    export default function imageUrl(imageId, { size = 'normal' }) {
    if (imageId == null) return null

    const state = store.getState()
    const imagesRoot = state && state.constants && state.constants.imagesRoot
    if (imagesRoot == null) return null

    return `${imagesRoot}/${imageId}?size=${size}`
    }
    ```

    - **Pro**: Fast
    - **Pro**: No `async`
    - **Pro**: Pretty easy to test by injecting state into the `store`
    - **Con**: Tied to Redux. In fact, it's tied to an instance of the Redux store being available at `my/redux/store`
    - **Con**: Temporal coupling. If you call the function before dispatching the `fetchConstants` action that populates `state.constants.imagesRoot`, you get `null` back.

    h3. Option Four

    We can keep the function _pure_ by using Currying:

    ```js
    export default function imageUrlForRoot(imagesRoot) {
    return function imageUrl(imageId, { size = 'normal' }) {
    if (imagesRoot == null || imageId == null) return null
    return `${imagesRoot}/${imageId}?size=${size}`
    }
    }
    ```

    Applications that don't use Redux can still use it:
    ```js
    imgTag.src = imageUrlForRoot('https://example.com/images')('cheese.png')
    ```

    For applications that _do_ use Redux, we can partially apply the function in a reducer:

    ```js
    // reducers/constants.js
    import imageUrlForRoot from 'lib/image-url'

    export function constants(state = {}, action) {
    switch (action.type) {
    case 'RECEIVE_CONSTANTS':
    return {
    ...state,
    imageUrl: imageUrlForRoot(action.payload.imagesRoot)
    }
    default:
    return state
    }
    }
    ```
    And consume it in a component:
    ```jsx
    // Image.jsx
    import { useSelector } from 'react-redux'

    export default function Image({ imageId, alt, size }) {
    const imageUrl = useSelector('constants.imageUrl')
    const src = imageUrl(imageId, { size })
    return <img alt={alt} src={src} />
    }
    ```

    Have you done this? Do you like the idea of putting partially-applied functions into the Redux store? Do you have a better alternative?