Reputation: 753
I want to create a custom hook, let's say useTextProcessor(initialText, props)
.
It's a react state for storing & manipulating text (string).
It uses useReducer
for making cumulative state.
The code is like this:
interface TextEditorProps {
disabled?: boolean
onTextChanged?: (text: string) => void
}
const useTextProcessor = (initialText: string, props: TextEditorProps) => {
const [state, dispatch] = useReducer(textReducerFunc, /*initialState: */{
text : initialText,
props : props, // HACK: a dependency is injected here
});
state.props = props; // HACK: a dependency is updated here
return [
state.text,
dispatch
] as const;
}
There is a hack way to inject props
to be accessed in textReducerFunc
.
The textReducerFunc
is the main function where the text is processed (depended on the action type & props state).
I've no idea how to insert the dependency props
into textReducerFunc
in professional react way.
If I declared the textReducerFunc
inside the useTextProcessor(initialText, props)
,
yes I can access the props
, but wait since it's a child function, the child always be re-created every useTextProcessor
called.
This makes useReducer
executing the textReducerFunc
twice on the next render.
Ut's making onTextChanged
will be executed twice too.
Wrapping with useCallback
will not have any effect since onTextChanged
accepts any function (might be a static or inline function). The inline function is always different by reference on every render.
Here the detail of textReducerFunc
works:
interface TextState {
props: TextEditorProps // holds my hack
text: string // the actual data to be processed
}
interface TextAction {
type: 'APPEND'|'UPPERCASE'
payload?: string
}
const textReducerFunc = (state: TextState, action: TextAction) => {
const props = state.props;
if (props.disabled) return state; // disabled => no change
switch(action.type) {
case 'APPEND':
const appendText = state.text + action.payload;
props.onTextChanged?.(appendText); // notify the text has been modified
return {...state, text: appendText};
case 'UPPERCASE':
const upperText = state.text.toUpperCase();
props.onTextChanged?.(upperText); // notify the text has been modified
return {...state, text: upperText};
default:
return state; // unknown type => no change
} // switch
}
The usage of useTextProcessor
:
export default function TxtEditor(props: TextEditorProps) {
const [text, dispatch] = useTextProcessor('hello', props);
return (
<div>
<div>
{text}
</div>
<button onClick={() => dispatch({type: 'APPEND', payload: ' world'})}>append 'world'</button>
<button onClick={() => dispatch({type: 'UPPERCASE'})}>uppercase</button>
</div>
)
}
Can you suggest to me how to use useReducer
with dependency without any hack?
Upvotes: 0
Views: 802
Reputation: 4122
I would recommend elevating the state into the parent component. You need an "onTextChange" listener. That tells me that the state is in the wrong component. I recommend putting it into the parent component.
type TextAction =
| { type: 'APPEND'; payload?: string }
| { type: 'UPPERCASE' }
| { type: 'SET_DISABLED'; disabled: boolean };
interface TextState {
text: string;
disabled: boolean;
}
const textReducerFunc = (state: TextState, action: TextAction): TextState => {
console.log(action);
switch (action.type) {
case 'APPEND': {
if (state.disabled) return state;
return { ...state, text: state.text + action.payload };
}
case 'UPPERCASE': {
if (state.disabled) return state;
return { ...state, text: state.text.toLocaleUpperCase() };
}
case 'SET_DISABLED':
return { ...state, disabled: action.disabled };
}
};
function useTextReducer() {
return useReducer(textReducerFunc, {
disabled: false,
text: ''
});
}
function TextBox({
text,
dispatch
}: {
text: string;
dispatch: Dispatch<TextAction>;
}) {
return (
<React.Fragment>
<div>{text}</div>
<button onClick={handleAppendPress}>append 'world'</button>
<button onClick={handleUppercaseClick}>uppercase</button>
</React.Fragment>
);
function handleAppendPress() {
dispatch({ type: 'APPEND', payload: 'world' });
}
function handleUppercaseClick() {
dispatch({ type: 'UPPERCASE' });
}
}
function App() {
const [state, dispatch] = useTextReducer();
const { text } = state;
useEffect(() => {
console.log(`the text changed: ${text}`);
}, [text]);
return (
<React.Fragment>
<label>is disabled: </label>
<input
checked={state.disabled}
type="checkbox"
onChange={handleDisabledChange}
/>
<TextBox dispatch={dispatch} text={state.text} />
</React.Fragment>
);
function handleDisabledChange() {
dispatch({ type: 'SET_DISABLED', disabled: !state.disabled });
}
}
From what I see in your code (the buttons that are already in it) you probably want to move the "disabled" checkbox into the TextBox
component, further removing redundancy in state setting.
Upvotes: 1