react-streamfield
react-streamfield copied to clipboard
Code review
Alright! 🙂 I generally don't have the chance to review a whole package, that was a lot of fun. In the end I tried to be as exhaustive as possible with my comments, but a lot of it is fairly minor / quick to address.
I made the review as a pull request that contains all of the package's code, over at https://github.com/thibaudcolas/react-streamfield/pull/1, so it would be easier to relate comments to code. But I also summarised most of the comments below, so they are easier to relate to one another, and prioritise. It's probably easier to first read this list, then the comments in the PR.
I also made a PR to address some of the issues below (packaging, dev tools, and API), over at #5, along with the corresponding changes to Wagtail in a branch that builds upon https://github.com/wagtail/wagtail/pull/4942.
Potential problems
These are the code-level problems I would expect to cause the most pain / actual bugs. They are ordered by how important I think they are to address sooner rather than later.
- [x] Make an API for the package instead of doing the Wagtail integration in it (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249224118, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249299937)
- [x] Overly generic class names across all of the UI (e.g.
icon
,field
,required
) – these should use BEM, and-or be namespaced (see "Styles" section below, and https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249085282, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249216525, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249217050, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249217966, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249228525, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249224192, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249228525) - [x] Do not use elements like
aside
,article
,header
. They provide no semantic value for a form / widget like StreamField (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249228742) - [ ] ~
AddButton
'sgroupedBlockDefinitions
– What happens if key is an empty string? It seems like this is going to override the groups (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249217714)~ - [x]
extractText
– Use eithertextContent
orinnerText
depending on the desired outcome (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249220099) - [x] Add
CustomEvent
polyfill to Wagtail for IE11 support, or change the code not to need it (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249221668) - [x] Add an API for Wagtail to provide translations (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249228555)
- [x] Button labels should be provided by Wagtail, so they are localised instead of english-only (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249228555)
- [x] Disabled buttons in block actions should be disabled with the corresponding HTML attribute rather than CSS (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249299479)
- [x] Replace deprecated
initEvent
with event constructor (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249221460) - [x] Move
StreamField
constructor side-effects tocomponentDidMount
(see react-redux section below) (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249225067) - [x] See whole "Packaging" section below
Performance
- [ ] ~Revisit the AnimateHeight implementations to only render their children to the DOM when they are meant to be displayed (AddButton's panel, Block children) (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249299231, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249299377, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249299536)~
- [x] Clean up listeners / observers set up in RawHtmlFieldInput
componentDidMount
, which can cause memory leaks (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249299793) - [x] Small thing – do not render the add panel's group name if it's an empty string (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249299234)
Error handling
Generally I haven't seen much error handling code. I would expect the inner script execution to be the most problematic, since it will be very common for third-party code to break.
- [ ] Add error handling to inner script execution (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249226724)
- [ ] Add error handling to RawHtmlFieldInput's
componentDidMount
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249226953) - [ ] Add error handling of
moveBlock
action (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249222487) - [ ] Ideally, add error handling to all of StreamField (
componentDidCatch
?) for unforeseen sources of error
Minor ones
Minor but still sources of concern
- [ ] ~Preserve script tags in inner script execution instead of using
eval
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249226678)~ - [x]
<BlocksContainer />
isn't valid HTML. We shouldn't encourage people to write HTML that doesn't pass validation. For a semantic-free token/placeholder, use anoscript
tag with a data attribute. (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249064376, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249225428) - [x]
getIsMobile
– Usewindow.matchMedia
to make sure the JS and CSS breakpoint definitions are in sync (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249219429)
Packaging - build & dependenceies
The general problem here is that the library is compiled as if it was an app, instead of a library, with all of its dependencies bundled instead of resolved by npm on install.
- [x] Stop bundling dependencies in the compiled/published code, these should be resolved as part of the dependency management and build of the consumer code (Wagtail’s) (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249068303)
- [x] Publish the package as both CommonJS and ES modules, with
main
andmodule
attributes inpackage.json
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249068303) - [x] Remove any side effects on import, and mark the package as
"sideEffects": false,
for Webpack consumers (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249068303) - [x] Expose the compiled source (
dist
) via npm only, not in version control (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249056812) - [x] Consider using Rollup instead of Webpack to achieve all of the above easily (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249080818)
- [x] Do not pin dependencies to exact versions in
package.json
. Instead, use apackage-lock.json
. (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249069920) - [x] Move non-dev dependencies to
dependencies
:classnames
,react-animate-height
,react-beautiful-dnd
,uuid
(react-redux
,redux
,redux-thunk
,react
,react-dom
,prop-types
) (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249072535) - [x] Move most fundamental and heavy dependencies to
peerDependencies
:react-redux
,redux
,redux-thunk
,react
,react-dom
,prop-types
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249072842, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249073093)
Bonus points
- [x] Consider using
sass
(official Dart implementation) to get rid of the annoying native code compilation ofnode-sass
. (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249073320) - [x] Consider configuring
transform-react-remove-prop-types
to wrap proptypes instead of removing them, as for a project like this they can be very useful in dev mode. (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249074356) - [x] Remove unused
@babel/cli
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249068654) - [ ] Consider replacing
@babel/plugin-proposal-decorators
and decorators syntax with normal higher-order functions/components. (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249070350)
Documentation
To me this is what would be the most worthy of documentation. The blockDefinitions
schema is probably this package’s most important API, and the polyfills are its least obvious requirements.
- [ ] Document the
blockDefinitions
schema (can be as simple as adding comments toBlockDefinitionType
, and linking to this) (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249060708) - [x] Document the need for a
position: sticky
polyfill in the project's README, and in https://docs.wagtail.io/en/v2.4/contributing/developing.html?highlight=Sticky (Wagtail doesn't come with one, it's very expensive perf-wise), for full IE11 support (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249088987) - [x] Document the need for the
element-closest
polyfill for IE11 support (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249216286) - [x] Document the need for the
Array#find
polyfill for IE11 support (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249220422) - [x] Document the need for the
Object#entries
polyfill for IE11 support (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249221113) - [x] Document the need for the
CustomEvent
polyfill for IE11 support (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249221668)
Development & demo env
- [ ] Consider setting up linting with ESLint and Stylelint, with a config similar to that of Wagtail (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249075440)
- [ ] Consider setting up Prettier for automated formatting (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249075440)
- [x] Consider swithing to Storybook for a better dev / demo environment (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249059934)
- [x] Consider removing
drop_console
setting from UglifyJS setup (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249077837) - [ ] Consider using SVG instead of FontAwesome for the icons (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249063304)
- [x] Add Autoprefixer to the CSS build (Wagtail already has it, so this is just about having a similar setup for this package’s dev/demo site, so it can be used for cross-browser testing) (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249076623)
- [x] Consider adding an example with a custom JS widget (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249065289)
Styles
- [x] Split
src/index.scss
into multiple files, ideally following the separation between React components (one styles file per component) (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249082093) - [x] Use BEM notation for element selectors, e.g.
.add-block-button
instead ofbutton.add
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249084002, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249087414) - [x] Use BEM notation for state selectors, e.g.
.children-container--dragging
instead of.children-container.is-dragging
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249084002, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249084730, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249085387) - [x] Replace bare element names by class names, e.g.
.block-header
instead ofheader
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249086253, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249087414) - [ ] ~Consider using a JS-added
--focus
class instead of:focus-within
, if the functionality is important enough (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249085971)~
Accessibility
I'm sure the current StreamField implementation isn't particularly SR-friendly, so we're not aiming super high, but there are obvious improvements to be made here.
- [x]
AddButton
’s + icon should have a text-only label for screen reader users (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249217189) - [x] All other icons should also have text-only labels for screen reader users (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249227560)
- [x] All icons should be marked as
aria-hidden="true"
so screen readers ignore them (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249227560, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249227422) - [ ] All icons should be built with SVG instead of an icon font (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249227560)
react-redux
react-redux recently released its v6, which uses the new React context API from React 16.4, and introduces a change in behavior that affects this package:
[...] there is a behavior change around dispatching actions in
constructors
/componentWillMount
. Previously, dispatching in a parent component's constructor would cause its children to immediately use the updated state as they mounted, because each component read from the store individually. In version 6, all components read the same current store state value from context, which means the tree will be consistent and not have "tearing". This is an improvement overall, but there may be applications that relied on the existing behavior.
This is the problematic code:
https://github.com/noripyt/react-streamfield/blob/9b720dd715140f5c931560e75431ea472600f9be/src/StreamField.js#L95-L107
The consequence is that BlocksContainer
will fail to render because it does not expect to have access to an uninitialised state. It's generally not recommended to have side-effects in the constructor anyway, moving this init to componentDidMount
would make the problem even more obvious.
I can see a few solutions:
- Move the
initializeStreamField
logic out ofStreamField
, to be done when the store is created - Move
initializeStreamField
call tocomponentDidMount
, and do not render theBlockContainer
until initialisation is over.
Anyway, it's not necessary to upgrade to v6 now. I also have two concerns with the upgrade:
- react-beautiful-dnd also uses Redux and react-redux v5, but doesn't declare them as
peerDependencies
. Using different versions from it would mean they get bundled twice for end users, which I would rather avoid. From memory Redux is fairly small in size, butreact-redux
is a good 20kb. - Wagtail uses v5, and switching to v6 will require updating Wagtail's code. Shouldn't be a big deal, but it will take a bit of time.
Finally on the react-redux front, I'm surprised that all/most of the components in the package are connected. I would expect the performance to be better if some of the connections were removed, as they clearly duplicate their computation (but use PureComponent
or React.memo
to still have the same rendering performance)
- [ ] Remove connection of
BlockContent
, and use props fromBlock
instead (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249228217) - [ ] Remove connection of
BlockHeader
, and use props fromBlock
instead (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249228076) - [ ] Remove connection of
BlockActions
, and use props fromBlock
instead (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249228398) - [x] Remove connection of
RawHtmlFieldInput
, and use props fromFieldInput
instead (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249225731)
This would also make it easier to write tests for those components, which I would really like to see.
Smaller JS / React things
- [x] Consider using updater callbacks for
setState
calls such asthis.setState({open: !this.state.open});
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249215911) - [ ] Consider binding the
addHandler
's key to the function, instead of storing it and reading it in HTML. (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249218390) - [x] It's more idiomatic to check for null/undefined ("falsey") values by doing
if (value)
instead ofif (value === null)
,value !== undefined
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249213731, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249216741, https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249216858) - [x] There are other idiomatic JS comments I've added along the way.
- [x] Consider using the latest syntax for React fragments (
<></>
) (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249217474) - [ ] ~Remove unneeded boolean parameter of
getDescendantsIds
(always called withtrue
) (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249218850)~ - [ ] Split
getNewBlock
of the utils into smaller functions (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249220662) - [x] Remove unneeded prefixed fallbacks for
MutationObserver
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249225568) - [x] Do not use
h3
for BlockHeader's icon (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249227345) - [ ] Extract block title rendering logic, and add tests for it (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249228108)
- [x] Use a switch statement for the reducer (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249223229)
- [x] Rename
actions.js
toreducers.js
(https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249222891) - [ ] Unneeded deepCopy? (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249223174)
- [ ] Use FSA for actions like Wagtail (https://github.com/thibaudcolas/react-streamfield/pull/1#discussion_r249223503)
This PR https://github.com/noripyt/react-streamfield/pull/6 should resolve the following:
- ✅ Overly generic class names across all of the UI (e.g. icon, field, required) – these should use BEM, and-or be namespaced. Everything is now using BEM and is namespaced with
c-sf
inline with our new ITCSS inspired structure. Wheresf
stands forstreamfield
. - ✅ Do not use elements like aside, article, header. They provide no semantic value for a form / widget like StreamField
- ✅ Use BEM notation for element selectors, e.g. .add-block-button instead of button.add
- ✅ Replace bare element names by class names, e.g. .block-header instead of header
- ✅ Do not use h3 for BlockHeader's icon There does seem to be a case where a heading level is reasonable so I've removed it only when it's the icon only variant.
- ⚠️ Split src/index.scss into multiple files, ideally following the separation between React components (one styles file per component) I've done this for the most part - but haven't quite made a separate component that exactly matches each react file. In some cases multiple SCSS components just made sense to separate them because they are very visually different components - eg
.c-sf-add-button vs .c-sf-add-panel
. In others there were technical reasons -.c-sf-block
in particular has a lot of children that are inter-dependant. Pulling them out into multiple files made it more complex to parse. I left some notes about this in the SCSS for that component for things we could do to improve this if need be but I don't think they should block release. - ⚠️ Use BEM notation for state selectors, e.g. .children-container--dragging instead of .children-container.is-dragging As discussed in Slack, I've gone with
is-/has-
state classes in most cases. Both methods seem to be in use across the Wagtail code base and so I went with the least effort case until we review our CSS/JS styleguide. Happy to be overridden here :)
@thibaudcolas would be good to get you to confirm and provide your thoughts :)
Sweet, I'll try to have a look this weekend!
Thank you very much to both of you for these contributions!
@thibaudcolas I improved a lot of things as well after @jonnyscholes’ contribution. I updated the checkboxes above. As you can see, there is mainly one important thing remaining that I plan to do after we release the first integration to Wagtail: the error handling. I will go through the same incremental updates you did for Draftail.
I leave this review open for me to fix the remaining unticked checkboxes. But for now, this is already more than good to be in Wagtail :)