Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove withRouter (1/n) #3007

Merged
merged 25 commits into from
Dec 3, 2020
Merged

Remove withRouter (1/n) #3007

merged 25 commits into from
Dec 3, 2020

Conversation

taneliang
Copy link
Member

Context

As #2922 requires extensive changes to the whole codebase, I've decided to split it into multiple PRs.

This PR works towards React Router 6 compatibility by removing calls to withRouter. RR6 is necessary for preloading support, which is necessary in #2922. This is incomplete and will be continued in a future PR, but I think it may be best to get some feedback on the current work first.

Implementation

  1. Frontend tests of hoc-wrapped components will likely need to be changed significantly, because:

    1. With hooks, we can't e.g. pass in history and Redux state props. We need to use MemoryRouter/Router and Redux's Provider instead.
    2. Tests frequently assume that data will be passed in as props, which may no longer be the case once we switch to Relay/Apollo. Tests that are written from the end user's perspective will help us prepare for this.
    3. Some tests assume the component will be a class component, and test their methods directly.
  2. Added @testing-library/react so that it's easier to test components in context and from a user's perspective.

  3. makeResponsive is deprecated with a new useMediaQuery hook.

  4. A new global __TEST__ variable is declared, as we sometimes need to distinguish between __DEV__ and __TEST__.

GlobalSearchContainer tests have been rewritten with this new approach. Would appreciate feedback!

For a more modern testing API that:

1. Discourages testing implementation details.
2. Makes it easier to test user-visible behavior.
Prepares GlobalSearchContainer tests for functionalization. The existing
tests tested methods on the GlobalSearchContainer class, which were an
implementation detail (those methods were meant to be called by the
child `GlobalSearch` component). Those methods will be turned into
`useCallback` callbacks in a functionalized component not accessible
from outside the component, requiring a rewrite of the tests.
Preparing to replace those HOCs with hooks.
@taneliang taneliang requested a review from a team December 1, 2020 10:13
@codecov
Copy link

codecov bot commented Dec 1, 2020

Codecov Report

Merging #3007 (f019ae1) into master (089eb58) will increase coverage by 1.42%.
The diff coverage is 26.90%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3007      +/-   ##
==========================================
+ Coverage   46.35%   47.78%   +1.42%     
==========================================
  Files         252      254       +2     
  Lines        5324     5316       -8     
  Branches     1224     1218       -6     
==========================================
+ Hits         2468     2540      +72     
+ Misses       2856     2776      -80     
Impacted Files Coverage Δ
website/src/.eslintrc.js 0.00% <ø> (ø)
website/src/actions/venueBank.ts 33.33% <ø> (ø)
website/src/bootstrapping/configure-store.ts 68.75% <ø> (+68.75%) ⬆️
website/src/bootstrapping/sentry.ts 0.00% <0.00%> (ø)
website/src/entry/main.tsx 0.00% <0.00%> (ø)
website/src/utils/react.tsx 72.97% <0.00%> (-5.98%) ⬇️
website/src/views/AppShell.tsx 0.00% <0.00%> (ø)
website/src/views/components/KeyboardShortcuts.tsx 0.00% <0.00%> (ø)
...ribute/ContributeContainer/ContributeContainer.tsx 0.00% <0.00%> (ø)
website/src/views/modules/ModulePageContent.tsx 74.41% <ø> (ø)
... and 23 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 089eb58...f019ae1. Read the comment docs.

@nusmods-deploy-bot
Copy link

nusmods-deploy-bot bot commented Dec 1, 2020

Comment on lines -37 to -39
export function queryMatch(query: QueryObject) {
return window.matchMedia(json2mq(query));
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code originally used in module search before we moved to Elasticsearch


// Fetch the module data of the existing modules in the timetable and ensure all
// lessons are filled
const timetables = store.getState().timetables.lessons;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not using useSelector here, because if we bind timetables outside this hook:

  1. We'll need to pass timetables as a dependency to useCallback, which will cause the callback to change when the user modifies the timetable. This will cause the effect below to be executed, which is not what we want.
  2. If we don't pass timetables as a dependency, the callback may capture a old copy of timetables, which will cause the incorrect modules to be fetched.

website/src/views/components/KeyboardShortcuts.tsx Outdated Show resolved Hide resolved
expect(window.scrollTo).toHaveBeenCalledTimes(1);
expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
history.push('/foo');
act(() => history.push('/foo'));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

act is needed to flush effects

Copy link
Member

@ZhangYiJiang ZhangYiJiang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, this is truly a heroic effort. Props to you for doing this :P

website/src/utils/css.ts Outdated Show resolved Hide resolved
website/src/utils/css.ts Outdated Show resolved Hide resolved
website/src/views/components/KeyboardShortcuts.tsx Outdated Show resolved Hide resolved
Comment on lines 11 to 12
const [Kawaii] = useState(() => sample(icons) ?? icons[0]);
const [defaultMood] = useState(() => sample(defaultMoods) ?? defaultMoods[0]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using state to only initialize once instead of using refs is rather weird lol. It looks like there's an easy way to write a hook for this that does it correctly facebook/react#14490 (comment).

Also array access like this is not really safe anyway (if sample returns undefined then the array is empty, so [0] would also return undefined), so just use !

Copy link
Member Author

@taneliang taneliang Dec 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using state to only initialize once instead of using refs is rather weird lol.

Hmm, I'm not sure if the additional code overhead is worth it. Also looks like someone else also suggested using useState in that issue 😆. IMO useState is the best solution here as conceptually what we want is literally state that we don't want to change. I don't think it's exactly a ref either as refs tend to imply some sort of imperative/mutable variable.

Also array access like this is not really safe anyway (if sample returns undefined then the array is empty, so [0] would also return undefined), so just use !

Yeah I just wanted to avoid the noise of eslint-disable, but sure that works too. What we really want is nullthrows but probably it's probably not worth adding it for just one callsite.

website/src/views/components/ScrollToTop.tsx Outdated Show resolved Hide resolved
website/src/views/layout/GlobalSearchContainer.test.tsx Outdated Show resolved Hide resolved
/**
* Fetch module list on mount.
*/
function useFetchModuleListAndTimetableModules(): {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to get rid of connect? This feels a fair bit more awkward then the old code, though having good typings is nice

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What feels awkward? If this hook is weird, maybe we could just inline it into the AppShell component. I just thought it was nicer to isolate the fetching logic into its own hook so that we didn't have so many variables lying around. I don't think we need to get rid of connect, but I don't think keeping it around helps much?

website/src/views/hooks/useScrollToTopEffect.ts Outdated Show resolved Hide resolved
jest.mock('utils/react');
const mockedScrollToHash = scrollToHash as jest.MockedFunction<typeof scrollToHash>;

const Tester: FC = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe consider using the hook testing library https://react-hooks-testing-library.com/usage/basic-hooks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think having an explicit tester component is clearer? I didn't think using that library helps much since this component is only 2 lines long, and we'll still need to provide a Router wrapper (similar to the snippet below from hook testing library docs). The additional indirection seems to provide little value in this case.

  const wrapper = ({ children }) => <CounterStepProvider step={2}>{children}</CounterStepProvider>
  const { result } = renderHook(() => useCounter(), { wrapper })

website/src/views/components/RandomKawaii.tsx Outdated Show resolved Hide resolved
Comment on lines +42 to +43
draft.moduleBank.moduleList = (storeOverrides.moduleBank?.moduleList ??
relevantStoreContents.moduleBank.moduleList) as typeof draft.moduleBank.moduleList;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. I guess it is possible to fix the typing of MODULES, but it's a bit awkward (we need to add this util type microsoft/TypeScript#24509). Cast is probably fine too, since this is just a test

Comment on lines +74 to +77
let onMediaChangeCallback: () => void;
const addEventListener = (_type: string, listener: (...args: unknown[]) => void) => {
onMediaChangeCallback = listener;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be simpler to just mock the entire hook, but this does also add some coverage to the hook itself, so 🤷

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably should add tests for the hook itself and simplify the tests here. Was intending to do it but forgot. I'll file an issue to fix another time

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created issue #3011.

@taneliang
Copy link
Member Author

Thanks for the quick reviews @ZhangYiJiang 🏃‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants