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. ### 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. ### 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. ### 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. ### 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/helpers.js import imageUrlForRoot from 'lib/image-url' 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 } } ``` And consume it in a component: ```jsx // Image.jsx import { useSelector } from 'react-redux' export default function Image({ imageId, alt, size }) { const imageUrl = useSelector('helpers.imageUrl') const src = imageUrl(imageId, { size }) return {alt} } ``` - **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**: 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?