TLDR: this explains how to solve the problem without redux by using react context in typescript (you can just convert it back if you prefer js). Hope this misses the topic not that much, since I was also stumbling upon this thread some time ago.
In the specific case of handling file uploads in forms, I would suggest to not use the state manager (non-serialized data) and go for react context. With this you could pass up the file / array of files (multiupload) and use where ever you need them (in the scope of the provider ofc) and don't have to drill the props their way to the desired component.
I'm sure there are other ways to solve this, but the more I use react context, the more I had understood how things are working under the hood, and the the usage principle compared to a state manager feels just a stone throw away. But let's remember, with a slightly different scope of requirement, because we wanna upload some files and store it in the browser, maybe even preview before we send them, like #jetpackpony described, and then the following comes handy:
Imagine a dialog component we wanna wrap with a context, so that child components can receive and process data in it's parents scope.
Create context & export our provider:
DialogContextProvider.tsx
import { createContext, useMemo, useState } from 'react'
interface DialogContextValues {
attachments: File[]
setAttachments: React.Dispatch<React.SetStateAction<File[]>>
}
export const DialogContext = createContext<DialogContextValues | undefined>(undefined)
const DialogContextProvider = ({ children }: { children: JSX.Element }) => {
const [attachments, setAttachments] = useState<File[]>([])
const dialogContextValue = useMemo(
() => ({ attachments, setAttachments }),
[attachments, setAttachments],
)
return <DialogContext.Provider value={dialogContextValue}>{children}</DialogContext.Provider>
}
export default DialogContextProvider
Setup the context
As a parent of the components which then could get/set it's data.
SomeUIComponent.tsx
import DialogContextProvider from './DialogContextProvider'
export const SomeUIComponent = () => (
<div>
<h2>This is out of context</h2>
<DialogContextProvider>
<SomeComponentWhichUsesContext />
<AnotherComponentWhichUsesContext />
</DialogContextProvider>
</div>
)
context hook use
Addionally we can use custom hook to retrieve the data more streamlined in the code.
useDialogContext.ts
import { useContext } from 'react'
import { DialogContext } from './DialogContextProvider'
// Custom hook to access the created context
const useDialogContext = () => {
const context = useContext(DialogContext)
const attachments = context?.attachments || []
const setAttachments = context?.setAttachments || (() => [])
if (!context) {
throw new Error('useDialogContext must be used within a Dialog')
}
return { attachments, setAttachments }
}
export default useDialogContext
later you can consume the values you passed to the context like this
const { attachments, setAttachments } = useDialogContext()
Please note that you should only prefer react context over a state manager in smaller scopes of your app, since it will likely trigger more rerenders than a well configured state manager would do and lacks nice features like unidirectional data flow and a predictable, centralized state.