-
Notifications
You must be signed in to change notification settings - Fork 33
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
OEP 0028: Content theming in React (Draft) #79
OEP 0028: Content theming in React (Draft) #79
Conversation
Thanks for the pull request, @taranjeet! I've created OSPR-2565 to keep track of it in JIRA. JIRA is a place for product owners to prioritize feature reviews by the engineering development teams. Feel free to add as much of the following information to the ticket:
All technical communication about the code itself will still be done via the GitHub pull request interface. As a reminder, our process documentation is here. |
@taranjeet Thank you for the proposal. Feel free to ping me when you feel it is ready for an edX review |
Decision | ||
-------- | ||
|
||
* We will build small modular components which will be non-customizable, stateless and contain HTML, and certain logic. We can see an example of a smaller component SiteLogo, which will later be used in a customizable component _Header. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not really explained here that there are meant to be two types of components: internal/non-customizable and customizable ones.
At first read, this sentence just sounds like "we will build everything out of small components that cannot be customized."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added a point before this, where it mentions that there will be two types of component.
|
||
* We will provide support for a global redux store which will be Open edX global redux store. This will act as a central place to store data and will have access to all data available to the system. | ||
|
||
* We will consider the layout of the data in the Open edX global redux store as a stable API. We will provide support to pre-fill the store with some common data like current user, current course, list of courses enrolled, etc. We will provide the choice for themes to fetch data that's not part of the Open edX global redux store from REST API's using built-in redux actions and store it in their own separate redux store. We will announce breaking changes if the layout of the data changes in global store. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The redux store would be specific to each frontend (LMS, Studio, ecommerce, etc.) and not global to all of "Open edX".
using built-in redux actions and store it in their own separate redux store
Actions tend to be fairly coupled to the reducers and overall store state, so I'm not sure how useful it would be to provide standard action [creator]s but expect them to work with custom stores/reducers. It might be better to just provide a standard API client library for the REST APIs, without redux actions, and let each custom theme that needs to use them include its own redux actions/reducers/state. But maybe this would help reduce boilerplate etc., and most any reducer could be adapter to ingest standard actions. Curious to know what others think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we can expect that there will be 100% coverage for each API in the frontend, so people will have to create their own actions. I don't think it's reasonable to have the actions and reducers etc for data not used by the default theme. Where a built-in action exists, chances are that data is already loaded, and if not, the standard actions can perhaps be used.
While an API client could be automatically generated using Swagger or such, I think there should probably be multiple API clients for multiple sets of APIs because the API is huge so why should all that code be loaded if most of it isn't used. If a custom theme uses some API that the default theme doesn't, it can also include the relevant API client. This probably should cover the whole API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xitij2000 @bradenmacdonald I have updated this point but I am not sure if it covers your concern. Please let me know your thoughts on this. Thanks.
|
||
* We will use containers [2] to access data from the redux store and provide it to components via props. A container is a react component that has a direct connection to the state managed by redux and access data from the state via mapStateToProps. We will use Container as a mechanism to separate data access functionality from the Component. This way we can keep both non redux connected version as well as redux connected version of the same component. | ||
|
||
* We will have support to convert any component into a container if it needs to access any data from the redux store, which it currently does not have access to. We can see this by an example where NavbarHeader component initially displays site title. This component now needs to display authenticated username, which is there in the redux store. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the example below should be showing a NavbarHeader component that doesn't use the username, and an inherited version that does. The example here doesn't work without the container, so it isn't an example of "converting any component into a container".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have updated this to include NavbarHeaderWithUserName
which extends from NavbarHeader
but completely overrides its render function. The extension will help in propagating other functionalities from NavbarHeader
to NavbarHeaderWithUserName
.
NavbarHeader
contains support to display username which is overridden by a container. This also brings us to a point where sometimes both the component(to display additional properties) and container needs to be overridden in case we need to display some additional data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also don't like the idea of "converting" we are not really converting anything, we are just adding another component that passes some props by default by fetching that data from the store. So it's standard component composition, you're wrapping an existing component with another.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xitij2000 : I have updated the "converting" part to "composition" now. Thanks:)
@mduboseedx : Thanks. Sure I will ping you when the proposal is ready for an edx review. :) |
); | ||
} | ||
|
||
* We will build the complete UI out of the above small modular components. We will generally use composition to build a customizable component. However, inheritance can be used at times. These customizable components will neither contain HTML nor logic. We can see an example of a customizable component called the _Header component, which is formed by the composition of SiteLogo, MainNav and UserAvatar component. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here the language should be "we will aim/prefer to use composition to build components wherever possible" and leave out the bits about inheritance. Instead, explain the difference in approach via an example.
I think we should also explain why this decision was made. Why prefer composition over inheritance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. What I have done is added about this in the other point and given an example of both cases. Thanks :)
.. code-block:: js | ||
|
||
// SiteLogo being updated in Header | ||
class MyThemedHeader extends _Header { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As @bradenmacdonald mentioned elsewhere, this is using inheritance not composition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps I think you are talking about this comment. That was for another point wherein the point I talked about using "composition" but gave an example of "composition" "inheritance". I updated it then only.
Also here it makes sense to use "inheritance" only since if we use composition we would end up creating components like BaseHeader
, Header
, SiteLogoHeader
and MyCustomAnimatedLogoWidgetHeader
.
Let me know if this is not right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point is that you mention in the paragraph: "We will prefer composition whenever adding any new component or content and inheritance when replacing or modifying the component." Immediately after that, you are showing an example of using inheritance when that is not the preferred approach. Show both ways of doing this, so that it's clearer which is the preferred approach in what circumstance.
I would also say this is not a good example of inheritance at all. You are extending an existing component, but there is nothing from the original component that you are actually using. Why couldn't this just be a new component? The only thing it gets from _Header
is the getNavLinks
getter method and that isn't even being used, the render
method is entirely replaced.
Also, I think if a component has a good chance of needing modification in some area, there should probably be a hook for it. So, in this case, replacing the Logo in the _Header
is a pretty obvious modification, so perhaps there should already be a prop for it, and should just default to the standard logo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xitij2000 : I am not sure here but I think we should skip this difference part between composition and inheritance since that is something more specific to React and not this proposal in general. The line where we can say that here inheritance or composition which one should be used is very blur.
Keeping this in mind, I think its better to just write via composition/inheritance
and leave it to the one who is implementing it because he/she will be a better judge of the situation.
I have also commented on the ticket about how we can use composition for the Header component but that turned out to be a discussion on inheritance vs composition.
class Header extends React.PureComponent {
public render() {
return (
<header>
{props.children}
</header>
);
}
}
const BaseHeader = (props) => {
<Header>
{props.children}
<MainNav/>
<UserAvatar/>
</Header>
}
const SiteLogoHeader = (props) => {
<BaseHeader>
<SiteLogo />
</BaseHeader>
}
const MyCustomAnimatedLogoHeader = (props) => {
<BaseHeader>
<MyCustomAnimatedLogo />
</BaseHeader>
}
I think Braden can help us here.
@bradenmacdonald : Your inputs please :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's important to have some conventions in mind because otherwise, things can get messy. It will decide what kinds of hooks a component will provide for instance. If you're expecting composition you'd want to provide more props to control parts of the component. In the case of inheritance, you'll want methods that can be overridden etc.
I think the React ecosystem is generally more aligned with composition as you suggested, so we should favour that, and this is precisely why it becomes important to explore under what conditions would inheritance be necessary. If it never is, we should just say that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that inheritance will be necessary any time you want to remove or replace an existing component child such as the SiteLogo
in the example above, unless there is some other hook for doing so via props (like logoOverride=<MyCustomAnimatedLogo />
) or modifying a global (like replacing a component globally via const SiteLogo = MyCustomSiteLogo; // not StandardSiteLogo;
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bradenmacdonald How will inheritance help in that case though? I might be missing something but here is how I see it:
Case: You have a component where the logo should be removable/replaceable.
- Provide a prop that allows replacing the logo. e.g.
<div> {this.props.logo === undefined?<DefaultLogo />: this.props.logo}</div>
. This allows setting the logo as null or providing a custom logo without inheritance. - You inherit from the original component. But now you're overriding the original component's render method. So what exactly do you gain by inheriting vs creating a new component? The use for inheritance would be with components that have a lot of logic that will be inherited, but customisable components won't have that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows setting the logo as null or providing a custom logo without inheritance.
Yes, but that assumes that the person who wrote the original component knew that people might want to override the logo. I'm sure there will be lots of other cases where the original component author didn't know what parts people might want to hide/replace/insert. So sometimes I think inheritance will be necessary.
But now you're overriding the original component's render method. So what exactly do you gain by inheriting vs creating a new component?
My original idea was that the render()
methods of customizable components should be so straightforward that they are easy to override completely, in order to give the theme complete control over removing, re-ordering, replacing, or inserting children. The logic wouldn't be directly inherited, but because the child components encapsulate the relevant logic, I think it would be pretty powerful.
Maybe we should come up with more concrete examples to figure this out better? It's still fairly theoretical at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should come up with more concrete examples to figure this out better? It's still fairly theoretical at this point.
Yup, I'm just having trouble seeing a component that has a such a straightforward render function, but it still makes sense to inherit, vs create a new component. I can see the case if there is some lifecycle code or other kind of complex code in the component, but I assumed that customisable components wouldn't have that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @xitij2000 -- inheriting from the default _Header
component in this example is confusing since it doesn't accomplish anything and the result is equivalent to class MyThemedHeader extends React.PureComponent { ... }
.
If we want to show a case where inheritance is useful we need a better example.
|
||
* We will provide support for a global redux store which will be Open edX global redux store. This will act as a central place to store data and will have access to all data available to the system. | ||
|
||
* We will consider the layout of the data in the Open edX global redux store as a stable API. We will provide support to pre-fill the store with some common data like current user, current course, list of courses enrolled, etc. We will provide the choice for themes to fetch data that's not part of the Open edX global redux store from REST API's using built-in redux actions and store it in their own separate redux store. We will announce breaking changes if the layout of the data changes in global store. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we can expect that there will be 100% coverage for each API in the frontend, so people will have to create their own actions. I don't think it's reasonable to have the actions and reducers etc for data not used by the default theme. Where a built-in action exists, chances are that data is already loaded, and if not, the standard actions can perhaps be used.
While an API client could be automatically generated using Swagger or such, I think there should probably be multiple API clients for multiple sets of APIs because the API is huge so why should all that code be loaded if most of it isn't used. If a custom theme uses some API that the default theme doesn't, it can also include the relevant API client. This probably should cover the whole API.
|
||
* We will use containers [2] to access data from the redux store and provide it to components via props. A container is a react component that has a direct connection to the state managed by redux and access data from the state via mapStateToProps. We will use Container as a mechanism to separate data access functionality from the Component. This way we can keep both non redux connected version as well as redux connected version of the same component. | ||
|
||
* We will have support to convert any component into a container if it needs to access any data from the redux store, which it currently does not have access to. We can see this by an example where NavbarHeader component initially displays site title. This component now needs to display authenticated username, which is there in the redux store. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also don't like the idea of "converting" we are not really converting anything, we are just adding another component that passes some props by default by fetching that data from the store. So it's standard component composition, you're wrapping an existing component with another.
@xitij2000 @bradenmacdonald This is ready for your review. Thanks :) |
Sounds good @nasthagiri, thank you for the information. Nice to hear the Architecture team is including theming in their Roadmap soon :) |
@nasthagiri @ormsbee Is this a good time to return to this PR or not yet? |
@natabene We are aware of these 2 theming-related proposals. We plan to address them as part of our replatforming runway. As indicated on our Roadmap Progress, we are currently focusing on our "Design System" (library of reusable components). We plan to address theming only after that - but still in the 1st half of this year as previously indicated. |
Also, for any of you following this PR, please contact me (via slack/email) if you'd like to join an Open edX "Theming Advisory Group". We plan to consult this group on requirements and thoughts on "theming" on the Open edX platform. Thanks. |
@nasthagiri Thank you for the update, super helpful to me. |
@nasthagiri I don't have access to the replatforming runway or roadmap progress docs. Has there been any progress on made on theming? |
@nasthagiri There has been some exciting development around micro-frontends in the past year. Is there any documentation or guidelines on how how to theme micro-frontends? |
@bradenmacdonald Are there elements of this proposed OEP that you believe are still valid given the latest developments in Open edX MFEs? If not, should we close this old Draft and reopen with the latest considerations? |
@nasthagiri My understanding is that newer guidance is here in extension_points.rst#Theming Microfrontends, and specifically that the proposals in this OEP would instead be replaced by "Frontend Plugins" or entire custom MFEs (as well as Overriding Brand Specific Elements). AFAIK frontend plugins still needs to be defined, but there's a proposal at openedx/frontend-app-discussions#5 I think it makes sense to close this and re-open when there's a solid, tested frontend plugins approach that we're ready to recommend more widely across MFEs. |
Thanks, @bradenmacdonald. I agree that the various efforts you enumerated will collectively form a basis for the extensibility and configurability of our frontends. Once those efforts are matured enough past their proof-of-concept phases (in ADR forms), we can create/update a frontend OEP that provides a cohesive narrative. Given that, I will close this PR. I'm looking forward to seeing these other efforts move forward. |
@taranjeet Even though your pull request wasn’t merged, please take a moment to answer a two question survey so we can improve your experience in the future. |
It aims to add guidelines for how to create and use React components, so that content modification becomes easy.
This OEP is based on this discussion and this gist
Note: As OpenCraft, our role is to define a proper solution and ensure that the future developments move in the right direction, but not committing on developing any of this at the moment.