Introduce React Incrementally - egnomerator/misc GitHub Wiki
We typically think "build a React App" rather than "use the JavaScript library React"
- we typically think of SPAs
- we typically think in terms of "A React App"--the whole app is a tree of React components
If using React, do we always have to write a "React App"? ... What if ...
- Existing App:
- what if you have an existing web app and want to start using React in it?
- maybe re-write the app?
- let's say in our scenario that isn't an option (sometime it is, sometimes it isn't)
- No Experience:
- what if everyone on your team has little to zero React knowledge/experience?
- maybe learn React and then re-write the app?
- let's say in our scenario that isn't an option (sometime it is, sometimes it isn't)
- Convert and Keep Delivering Features:
- what if you are on a team that needs to convert the app to a React App over time, while continuing to deliver new features to the existing app?
- maybe let part of the team focus on new features, and part of the team focus on the re-write in parallel?
- let's say in our scenario is continuing to deliver new features is a high enough priority that this approach would reduce new feature productivity a little too much for the business to accept this
- let's say the business considers converting to React a lower priority that can take longer so that delivering new features can stay a little closer to current levels of productivity
- note: "stay a little closer" meaning that the business does accept some reduced productivity of new features
Solution: Introduce React Incrementally
- this enables the development team to have control
- when to use React (in terms of sprint planning)
- how much to use React (how large of a chunk of UI/functionality)
- Existing App:
- the team can begin using React by either adding new features as React components, converting existing features into React components, or both
- No Experience:
- the team can start really small
- this can reduce risk of introducing bugs
- this can enable the team to use React in "digestible" amounts
- the team can build confidence over time as they increasingly use React at a safe pace
- Convert and Keep Delivering Features:
- by controlling when and how much to use React, the team can incorporate this into sprint planning
- starting small can
- enable new feature productivity to continue (reduced to an acceptable level)
- while enabling increasing the use of React over time
Okay Cool! How?
- NVM (optional) - i recommend using NVM to install/uninstall multiple Node.js versions and easily switch between versions
- Node.js (recommend LTS version; recommend all team members on same version)
- the version i used for the reference starter app (below) is v14.17.5 with NPM v6.14.14
- NPM (this is installed as a tool that comes with Node.js)
This reference solution is ready to be cloned and ran.
the solution: https://github.com/egnomerator/LibReactComponentsStarter
- there's a readme to help get started with the solution
- has the minimum NPM packages to support
- ES6 modules, React, JSX, TypeScript, Jest unit testing, and watch mode (for unit testing and/or debugging)
- a solid build process that is consistent across build scenarios
- has the minimum NPM packages as prod dependencies (just React)
- uses the folder and file structure that i've like most after looking at different common approaches
- defines a React components API for the rest of the app to use (webpack exposes it globally)
- demonstrates how to pass dependencies to React via this components API
- demonstrates how to enable React to use an Ajax library without having to use another NPM package
- it's common that React apps use an NPM package like axios for Ajax calls
- i prefer to minimize the use of NPM dependencies
- so i defined an object with a specific set of web API Ajax calls and passed this to React
- this web API object uses jQuery Ajax--so React is using jQuery for its Ajax calls
A key part of the above solution is global API that provides the ability to render an area of the page via React
- this source is where i explain how i'm setting up the API for the rest of the app to use
- the reference solution demonstrates this approach
(the reference app is already setup with these steps)
- add config files to Project Root folder (so at same folder level as the
wwwroot
folder)- package.json
- package-lock.json
- webpack.config.js
- babel.config.js
- tsconfig.json (for IDE intellisense support)
- tsconfigbld.json (for build process type-checking)
- tsconfigdts.json (for build producing declaration files--i don't have plans to use this though)
- note that these 2 files for "bld" and "dts" use the "extends" property referencing the main tsconfig.sjon file
- using this extends property means, use the referenced config but override with any differences specified in this file
- install NPM packages
- IF already have a package.lock file, use
npm ci
to install - otherwise i recommend
- install all npm packages with the --save-exact flag
- definitely install all devDependencies with the --save-dev flag
- IF already have a package.lock file, use
- update .csproj (incorporate JavaScript build process into .NET build process)
- add
wwwroot
folder to the Project Root folder if not already there- this step is probably not needed--the folder is probably already in the project
- create a
wwwroot/app
folder - and the folder/file structure under
wwwroot/app
folder (see next sections for folder/file structures)
(the reference app is already setup with this folder/file structure)
Top level folder: wwwroot/app
with 2 sub-folders
- sub-folder:
wwwroot/app/dist
(JavaScript build output will be generated under this folder) - sub-folder:
wwwroot/app/src
(source code in here)
The following structure under wwwroot/app/src
:
-
wwwroot/app/src/index.js
(top-level API--ClientApp
) -
wwwroot/app/src/components
(React components in here) -
wwwroot/app/src/components/index.js
(API to React components--ClientApp.Components
) -
wwwroot/app/src/components/FeatureName
(organize by Feature--containing one or many components)- there will be a folder for each component in this feature
-
wwwroot/app/src/components/FeatureName/FeatureName
- not a typo
- this is the "root" component for this feature--name it the same as the feature name
- ... remaining components in the feature
- remaining features
(the reference app is already setup with this folder/file structure)
A Feature
Feature: "TableCrudEditor"
- let's imagine we have a feature in our app for a table that provides CRUD operations
- the feature is called "TableCrudEditor"
Let's apply the folder structure for our feature--just the feature folder and the folder for each component in the feature:
-
wwwroot/app/src/components/tableCrudEditor
(feature folder) -
wwwroot/app/src/components/tableCrudEditor/TableCrudEditor
(root component for this feature)- this component could define the structure (using child components) of this area in the web UI
- this component could make Ajax calls and pass data to child components to display
- this component could define the high-level functions of the table--the CRUD functionality; refresh, etc.
- this component could maintain the state for this feature define how to operate on this state
-
wwwroot/app/src/components/tableCrudEditor/CreateItem
- this could be a component that contains input elements and a button that triggers creation of a new item
-
wwwroot/app/src/components/tableCrudEditor/TableCrud
- this could be the actual Table component that shows the items (rows)
- this table could use child components for its editable rows
-
wwwroot/app/src/components/tableCrudEditor/EditableTr
- this could be the row component (can be many of these in a table)
- this row could define cells as normal HTML td tags
- this row could additionally use a child component for a special cell that edits the row
-
wwwroot/app/src/components/tableCrudEditor/TrEditorCell
- this could be the special cell that edits the row
- it could contain buttons for editing, deleting, or viewing details of the item
(the reference app is already setup with this folder/file structure)
Capitalization: React component folders names are Pascal case--because React component names must be Pascal case
root component vs. the child-components
- Root
- the root component should be the orchestrator of the feature
- it will define the structure of this area of the web UI
- it will perform Ajax calls if needed, it will pass state, props, and functionality down to child components
- it will maintain and operate on the state of the feature
- child
- the child components should have very limited if any state, props, and functionality
note: by "module" i mean JavaScript module (e.g. es6 modules)
Root React Component (complex)
-
wwwroot/app/src/components/tableCrudEditor/TableCrudEditor
(component folder) -
wwwroot/app/src/components/tableCrudEditor/TableCrudEditor/index.js
- this is the actual component, it is a JavaScript class
- this component will use an instance of workflows class and pass itself as a reference
-
wwwroot/app/src/components/tableCrudEditor/TableCrudEditor/props.js
- separate props/state into their own modules to help keep file sizes smaller/readable
-
wwwroot/app/src/components/tableCrudEditor/TableCrudEditor/state.js
- separate props/state into their own modules to help keep file sizes smaller/readable
-
wwwroot/app/src/components/tableCrudEditor/TableCrudEditor/webApi.js
- if Ajax is a part of this feature, define a clean API for the web requests and separate this into its own module
-
wwwroot/app/src/components/tableCrudEditor/TableCrudEditor/workflows.js
- this is a JavaScript class
- this module needs a reference to the component it operates on
- this module will define the high-level functionality of the component
- this module will operate on the component's state
- this module imports activities and tasks (next)
-
wwwroot/app/src/components/tableCrudEditor/TableCrudEditor/activities
- example activity files
/activities/createItem.js
/activities/editItem.js
/activities/resetState.js
- each activity module handles a step or set of steps in a workflow
- activities have some knowledge of context--the context is the React component they operate on
- an activity module could potentially import another activity module
- example:
- when creating an item, you might want to clear the fields automatically
- similarly, you might want to do this when editing an item
- that
resetState.js
could represent this- it might take a number of steps to check existing state and make specific needed changes
-
createItem.js
andeditItem.js
could import theresetState.js
module
- example:
- an activity module will likely import one or more task modules (next)
- example activity files
-
wwwroot/app/src/components/tableCrudEditor/TableCrudEditor/tasks
- example task files
/tasks/getCopyOfItems.js
/tasks/updateMatchingItem.js
- a task has one function and requires what it needs as arguments
- unlike activities, tasks don't know about the component they are working for
- a task requires the minimum information to do its work
- a task does not get a reference to the React component
- a task does not import other tasks
- example task files
NOTE (about the workflow/activity/task organization):
- this is just a way to organize the functionality--there are plenty of other organization approaches
- this is similar to an approach i learned from a co-worker that our team has found very helpful
Child React Component (simple)
-
wwwroot/app/src/components/tableCrudEditor/EditableTr
(component folder) -
wwwroot/app/src/components/tableCrudEditor/EditableTr/index.js
- this is the actual component
-
wwwroot/app/src/components/tableCrudEditor/EditableTr/props.js
- only if needed--very often, a component does not need to have props or state
-
wwwroot/app/src/components/tableCrudEditor/EditableTr/state.js
- only if needed--very often, a component does not need to have props or state