How to Add Method to React Functional Component - reactjs

I have a component with code as such:
const Abc: React.FC<AbcTypes> = ({...}) => {...}
Abc.getLayout = () => {...}
I am unclear how to define/ extend the method getLayout on Abc component in Typescript?

If you are asking how to type this, then you could do this:
const Abc: React.FC<AbcTypes> & { getLayout: () => SomeType } = ({...}) => {...}
Abc.getLayout = () => {...}
If you are asking for a way to define imperative API for your components then useImperativeHandle is what you are looking for.
This will allow the parent component to attach a ref to Abc and call the methods you define.
Here is an example on how to use it:
type AbcRef = {
getLayout: () => string[]
}
const Abc = forwardRef<AbcRef>((props, ref) => {
useImperativeHandle(ref, () => ({
getLayout: () => ['abc'],
}))
return <div>Abc</div>
})
const Parent: FC = () => {
const abcRef = useRef<AbcRef>(null)
useEffect(() => {
console.log(abcRef.current?.getLayout())
}, [])
return <Abc ref={abcRef} />
}

Related

setting up React store using Immer

I am trying to set up store in React using immer and borrowed the code from another project. However it doesn't work for me. Could be that immer version on borrowed project is lower than it is now.
Store looks like this:
const initialState: MyState = {
search: "No results yet"
}
const useValue = () => useState(initialState);
const { Provider, useTrackedState, useUpdate: useSetState } = createContainer(
useValue
);
const useSetDraft = () => {
const setState = useSetState();
return useCallback(
(draftUpdater: MyState) => {
setState(produce(draftUpdater));
},
[setState]
);
};
export { Provider, useTrackedState, useSetDraft, initialState }
and hook that i use to mutate state is:
const UseSetSearch = () => {
const setDraft = useSetDraft();
return useCallback((searchString: string) => {
setDraft((draft: MyState) => {
draft.search = searchString
return draft
})
}, [setDraft]);
}
With the store, I get error
Type 'MyState' provides no match for the signature '(state: WritableDraft): ValidRecipeReturnType'
and hook error is
Value of type '(draft: MyState) => MyState' has no properties in common with type 'MyState'. Did you mean to call it?
Store error is fixed when I do:
const useSetDraft = () => {
const setState = useSetState();
return useCallback(
(draftUpdater: MyState) => {
setState(produce(draftUpdater, draft => {
}));
},
[setState]
);
};
but hook error remains. What am I missing?

How to spy only one react hook useState

I want to isolate the test to a targeted useState.
Lets say I have 3 useStates, of which some are in my component and some are in children components in this testcase.
Currently this logs for 3 different useStates. How to target the one I want. Lets say its called setMovies.
const createMockUseState = <T extends {}>() => {
type TSetState = Dispatch<SetStateAction<T>>;
const setState: TSetState = jest.fn((prop) => {
// if setMovies ???
console.log('jest - spy mock = ', prop);
});
type TmockUseState = (prop: T) => [T, TSetState];
const mockUseState: TmockUseState = (prop) => [prop, setState];
const spyUseState = jest.spyOn(React, 'useState') as jest.SpyInstance<[T, TSetState]>;
spyUseState.mockImplementation(mockUseState);
};
interface Props {
propertyToTest: boolean
};
describe('Search Movies', () => {
describe('Onload - do first search()', () => {
beforeAll(async () => {
createMockUseState<PROPS>();
wrapper = mount(
<ProviderMovies>
<SearchMovies />
</ProviderMovies>
);
await new Promise((resolve) => setImmediate(resolve));
await act(
() =>
new Promise<void>((resolve) => {
resolve();
})
);
});
});
});
as we know react hooks depends on each initialization position. And for example if you have 3 hooks inside your component and you want to mock the 2-nd, you should mock 1 and 2 with necessary data.
Something like this
//mock for test file
jest.mock(useState); // you should mock here useState from React
//mocks for each it block
const useMockHook = jest.fn(...);
jest.spyOn(React, 'useState').mockReturnValueOnce(useMockHook);
expect(useMockHook).toHaveBeenCalled();
// after that you can check whatever you need

Functions in a jest test only work when launched alone, but not at the same time

I have a custom hook that updates a state. The state is made with immer thanks to useImmer().
I have written the tests with Jest & "testing-library" - which allows to test hooks -.
All the functions work when launched alone. But when I launch them all in the same time, only the first one succeed. How so?
Here is the hook: (simplified for the sake of clarity):
export default function useSettingsModaleEditor(draftPage) {
const [settings, setSettings] = useImmer(draftPage);
const enablePeriodSelector = (enable: boolean) => {
return setSettings((draftSettings) => {
draftSettings.periodSelector = enable;
});
};
const enableDynamicFilter = (enable: boolean) => {
return setSettings((draftSettings) => {
draftSettings.filters.dynamic = enable;
});
};
const resetState = () => {
return setSettings((draftSettings) => {
draftSettings.filters.dynamic = draftPage.filters.dynamic;
draftSettings.periodSelector = draftPage.periodSelector;
draftSettings.filters.static = draftPage.filters.static;
});
};
return {
settings,
enablePeriodSelector,
enableDynamicFilter,
resetState,
};
}
And the test:
describe("enablePeriodSelector", () => {
const { result } = useHook(() => useSettingsModaleEditor(page));
it("switches period selector", () => {
act(() => result.current.enablePeriodSelector(true));
expect(result.current.settings.periodSelector).toBeTruthy();
act(() => result.current.enablePeriodSelector(false));
expect(result.current.settings.periodSelector).toBeFalsy();
});
});
describe("enableDynamicFilter", () => {
const { result } = useHook(() => useSettingsModaleEditor(page));
it("switches dynamic filter selector", () => {
act(() => result.current.enableDynamicFilter(true));
expect(result.current.settings.filters.dynamic).toBeTruthy();
act(() => result.current.enableDynamicFilter(false));
expect(result.current.settings.filters.dynamic).toBeFalsy();
});
});
describe("resetState", () => {
const { result } = useHook(() => useSettingsModaleEditor(page));
it("switches dynamic filter selector", () => {
act(() => result.current.enableDynamicFilter(true));
act(() => result.current.enablePeriodSelector(true));
act(() => result.current.addShortcut(Facet.Focuses));
act(() => result.current.resetState());
expect(result.current.settings.periodSelector).toBeFalsy();
expect(result.current.settings.filters.dynamic).toBeFalsy();
expect(result.current.settings.filters.static).toEqual([]);
});
});
All functions works in real life. How to fix this? Thanks!
use beforeEach and reset all mocks(functions has stale closure data) or make common logic to test differently and use that logic to test specific cases.
The answer was: useHook is called before "it". It must be called below.

Using `useImperativeHandle` to export ref

I have 2 following files, called A.js and B.js.
A.js
import RefFile from './components/RefFile'
const A = forwardRef((props, ref) => {
let refRefFile = useRef(null)
...
useImperativeHandle(ref, () => ({
refRefFile
}))
return (
...
<RefFile ref={refRefFile} />
...
)
}
B.js
import A from './A'
const B = () => {
let refA = useRef(null)
const test = () => {
refA?.refRefFile()
}
return (
<A ref={refA} />
)
}
Can I add the ref of RefFile into useImperativeHandle to call it from B? I think it will be more comfortable for me to do so. Or is there any alternative way to solve this one? Thank you!
You can work out something like this.
Let, foo() is a function in your RefFile.
A.js
import RefFile from './components/RefFile'
const A = forwardRef((props, ref) => {
let refRefFile = useRef()
...
const accessRefFileFoo = () => refRefFile.foo()
useImperativeHandle(ref, () => ({
accessRefFileFoo
}))
return (
...
<RefFile ref={refRefFile} />
...
)
}
B.js
import A from './A'
const B = () => {
let refA = useRef()
const test = () => {
refA.accessRefFileFoo()
}
return (
<A ref={refA} />
)
}
Here, you call the function named foo() which is a function in your RefFile from your component B. Like this, hope you will be able to handle this as you want. If you got any problem, feel free to comment here.

React hooks - indexed callback

useCallback is useful for memoizing functions to be passed to sub-components, but what if you're rendering those sub-components in a loop? e.g.
const setBlockedDate = useCallback(idx => ev => {
setBlockedDates(bd => arraySplice(bd,idx,1,[ev.value]));
},[setBlockedDates])
...
{blockedDates.map((bd,idx) => <VehicleBlockedDate
key={bd[REACT_KEY]}
defaultValue={bd}
onChange={setBlockedDate(idx)}
onDelete={deleteBlockedDate(idx)}
reasons={[]}/>)}
useCallback will only memoize the outer function, but not the inner one. Is there a helper function for this pattern? Because I don't think this is right:
const setBlockedDate = useCallback(idx => useCallback(ev => {
setBlockedDates(bd => arraySplice(bd,idx,1,[ev.value]));
},[setBlockedDates]),[setBlockedDates])
I think this is correct:
function useArrayCallback(fn, deps = []) {
return useMemo(() => memoize(idx => (...args) => fn(idx, ...args)), deps);
}
Where memoize is:
function memoize(fn, options={
serialize: fn.length === 1 ? x => x : (...args) => JSON.stringify(args),
}) {
const cache = new Map();
return (...args) => {
const key = options.serialize(...args);
if(cache.has(key)) {
return cache.get(key);
}
const value = fn(...args);
cache.set(key, value);
return value;
}
}
Or you can use some fancy-pants prebuilt memoize function.
Then the callback in the original question can be written as:
const setBlockedDate = useArrayCallback((idx,ev) => {
setBlockedDates(bd => arraySplice(bd,idx,1,[ev.value]));
})

Resources