-
Notifications
You must be signed in to change notification settings - Fork 824
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
RFC-9: UI Extensibility #4887
Comments
Note that we're preparing a separate RFC for "State management in the CMS" which will get into the Redux vs. Flux rationale a bit more. These three RFCs are quite tightly linked, since you can't suggest customisation options without making some calls on the base you're extending from :) |
They compile to the same (prototypical inheritance based) code. There is no language limitation on using prototypical inheritance or simulating multiple inheritance/mix-ins. class Table extends React.Component {
// ...
}
class DataGrid extends React.Component {
// ...
}
const hasOwnProperty = Object.prototype.hasOwnProperty;
for (let key in Table) {
if (hasOwnProperty.call(Table, key) && !hasOwnProperty.call(DataGrid, key)) {
DataGrid.prototype[key] = Table.prototype[key];
}
} ...is both allowed and readable. The objection is not syntactic but preferential. (code edits made all day long...) |
How do we feel about stateless components ( |
+1 on all of this. We're doing a React+Redux app right now and really thoroughly enjoying it. We've arrived at very similar conventions and I'm excited to see the CMS head that way too. Have you thought at all about REST calls vs Relay+GraphQL? +1 for allowing stateless components as well. |
On the surface it seems reasonable to use stateless components by default and only use 'stateful' components when you need to use life-cycle methods etc. There is the trade-off of having inconsistent syntax between the two component types which I don't really like. But on the other hand Facebook seem to be saying there will be performance optimisations made for stateless components. I've just got back from holiday today, so will get my head back into this and have a think about what other implications there might be, given the approach we've outlined 😄 |
Thanks @markguinn ! We haven't actively considered GraphQL, although it definitely looks interesting - we'll need to retrieve a lot of different data models with relatively slow API endpoints to build a feature rich CMS UI (menu, user profile, page tree, page data, allowed workflow actions, batch actions, etc.). Making all these as separate REST calls will get us into performance issues. I'm not convinced we have enough "graph-like" data to warrant GraphQL though. At the moment, our focus is with the CMS frontend. I'd expect API endpoints to be created as required through custom controllers for now - we just have to pick one battle with the 2.5 devs which can focus part-time on CMS UI here at SilverStripe Ltd. On that note, keen to contribute? :) Maybe a GraphQL PoC? Even just sharing knowledge about what worked for you on Redux will help immensely! @assertchris Can you explain how stateless components relate to UI extensibility? I think I understand why they're a good idea for UI architecture (easier to reason about, see https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html). But how are they helping extensibility? On ES6 classes: You're right, they're just a thin syntax layer on the usual JS prototypical inheritance, so they don't "enforce" anything :) |
@chillu - The app we're currently building uses REST endpoints just as you describe. We made that decision to make onboarding easier for new developers. That said, there have been numerous times I've thought "Ah, that's why they built Relay...". Relay allows individual components to declare what bits of data they need and then it composites them into larger queries at the root components. With REST, the server and parent components need to also know what data a child component needs so if that data changes you're potentially opening 3 or 4 files instead of 1. I suspect a manually built API is still the way to go at this stage for the reasons you mentioned and because it's a known quantity for security. If I get time or another paying project using these technologies I'll certainly throw together a PoC but I doubt it will be soon, unfortunately. As far as our usage of Redux, it looks like we're following many of the same conventions. We're not using DI but I think it's a great idea for the CMS. All of your examples look solid to me. |
@chillu This RFC seems to be all about recommended structure for third-party React CMS modules. Stateless by default is a structural consideration. If the intention of this RFC was only to define how third-party modules interact with an existing CMS, it creeped its scope. |
State management RFC #4911 |
Some thoughts on DI:
|
You don't need to: https://gist.github.com/assertchris/951058bc2eea72cc417149cd86465975 |
@assertchris So your example demonstrates that you can modify the DOM of a component by React component refs, right? That's not really influencing |
It is absolutely influencing render, but not in a way that the HoC knows anything about what render is supposed to be doing. Let me share another example of HoC influencing behavior, without owning the state or manipulating the decorated component: https://gist.github.com/assertchris/c7338c6499842abe636692deae61166e Refs are a powerful tool to trigger well-defined behavior from outside (or in HoC) components, but they're not the only tool we could use for decoration and/or modification of decorated components. Proxying between context and props is another way to do things like global plugin registries and plugin hooks... |
I've converted this card to an epic to reference the actual implementation cards, and updated this RFC to cross-reference to them. You'll need zenhub.io to see the related cards in the epic. |
If you followed along here, there's a good chance you're interested in the extensible GridField discussion |
We've come a long way with this, all cards associated with this epic are closed, and there's more focused discussion going on in the GridField RFC. Closing this. |
Author: @flashbackzoo @chillu
Status: Pending review
Version: 0.1
Purpose and Outcome
A SilverStripe CMS powered by a modern frontend JavaScript framework should retain its openness to developers wanting to customise and extend its presentation and UI behaviour. This proposal relies on the acceptance of RFC-8 (“Adding ReactJS to SilverStripe CMS”). It also assumes the use of the Redux state management library. A solid understanding of the Flux architecture approach which has evolved into the Redux library is recommended for reviewing this RFC.
A major goal of a new CMS frontend implementation will be decoupling client-side components from backend components. Most components will be rendered via frontend logic (React components), and consume structured data from the backend (rather than raw HTML generated in SilverStripe templates). Customising the user interface behaviour shouldn’t hence necessitate a PHP subclass or PHP-based modifications. For example a developer should be able to implement a character count in all form fields, or replace all form fields of a certain type with their own implementation and not have to touch server-side code.
Several key parts of the React application need to be extendible. We’ve put together a bare bones PoC so you can see some of these concepts is action.The PoC includes three modules: A common base module with shared functionality, a cms module demonstrating how the core would use these features, and a better-list module which could be a third party addition.
Customise Initial Application State
Developers can override and extend core state keys provided by the CMS and create their own. Note that this likely won’t be required often if we implement a simple configuration system which can be altered via custom static configuration files.
Customise Redux Actions
Developers can override and extend core Redux actions provided by the CMS and create their own. These actions are the only way for UI components to modify state. In order to enforce a well-understood unidirectional data flow, UI components don’t have direct access to the state they rely on (and might share with other components). Example actions are “page data received”, “select row” or “close URL segment edit view”.
Customise Redux Reducers
Developers can override and extend core reducers provided by the CMS and create their own. Reducers transform Redux actions (and their optional payload) to new application state, and are the only way for UI components to modify this state. Since Redux aggregates all reducers into one “root reducer” for the application, we propose that reducers can be chained via DIA (see nextReducer() call). The extending code can hence decide to call the original reducer or break the reducer chain for certain actions.
Customise React Components
Developers can override and extend ReactJS components provided by the CMS and create their own.
Customise React forms
SilverStripe can modify forms in PHP via getCMSFields(), but that approach is limited to a narrow band of declarative behaviour changes (e.g.
min
andmax
validation), and does not include JavaScript. Use SilverStripe'sFormBuilder
to modify form presentation and behaviour, and provide access to the underlying redux-forms layer. Details in Use FormBuilder to modify form field presentation.Customise GraphQL queries and retrieve new data
Retrieve new data on existing GraphQL queries in order to extend the UI. This only refers to the client-side customisations, not the PHP changes required for resolvers in the
silverstripe/graphql
module. See [GraphQL/Apollo RFC](#6245 for details.Customise GraphQL mutations and write new data
Similar to GraphQL reads, GraphQL based mutations ("writes") rely on GraphQL fragments. Adding new data requires modification of the underlying mutation queries. Hence the customisation approach should be very similar. Note that forms aren't currently submitted through GraphQL.
Customisation Approach
Static Configuration
Per-module configuration should allow a baseline customisation (e.g. sidebar width or availability of a “preview” panel view). This configuration should take the shape of a nested JavaScript object which is added to the application state on boot.
Composition through React Component Props
React components can be nested, and receive component instances as part of their props. We can use this to our advantage by allowing component composition. For example, a toolbar component could receive a collection of button components via a prop, allowing for addition or removal of buttons (see Building Plugins for React Apps)
Callbacks
React components can receive callback functions as props. This is commonly used for smart/dumb component interactions, e.g. a button click handler.
Registries and Middleware
Registries allow code outside of the core JavaScript bundles to modify behavior on app boot without recompiling these core bundles. They're effectively global state. Registries could take the form of middleware (passing state through a stack of middleware and optionally modifying it).
ES6 Class Extension
Since React components are just ES6 classes, they can be extended through the standard language constructs. Since ES6 classes enforce a single inheritance scheme on JavaScript’s prototypical inheritance, their use is limited (two modules couldn’t extend the same base class for a shared component).
Higher Order Components
The higher order component pattern wraps behaviour around an existing component, making their relationship more explicit and favouring composition over inheritance. This pattern works well for customising core components (e.g. text fields with a character count), but relies on the external component interface (React props). Higher order components can’t override methods, and are dependant on the underlying component exposing behaviour triggers via props and state.
Mixins
React Mixins have been deprecated with the 0.13 release, in favour of using higher order components. Mixins in JavaScript are powerful, but hard to debug and optimise.
Dependency Injection
Most of the above customisations rely on a structured way to hook into instance creation by third party code. For example, the core form builder React component would be in charge of creating a text field if required. If a third party module wants to customise the text field (e.g. replace it with its own React component), it needs a way to influence the created instance without direct access to the core form builder code.
We recommend the bottlejs dependency injection library to make the core application extendible. It can achieve the above extendibility requirements (and more) using out-of-the-box features, is well tested, well documented and relatively lightweight (1500 CLOC).
Service, factory, and provider patterns: Factories can be registered for each ReactJS component class we wish to make extendible. Developers can access these factories, and register their own, via a lightweight wrapper we provide around the bottlejs library.
Decorator pattern: The first time a dependency is requested from the DI container developers have a hook, which can be used to modify behaviour, or completely replace a dependency.
Middleware pattern: Similar to the decorator pattern. The middleware pattern provides a hook each time the dependency is requested from the DI container.
Namespaced containers:
The DI container can be namespaced if required. For example if we assume
myService
is a singleton thendi.container.foo.myservice !== di.container.bar.myservice
.Good writeup about using context for DI. The Griddle component library has some good use cases for deep React customisation via props.
Related
The text was updated successfully, but these errors were encountered: