-
Notifications
You must be signed in to change notification settings - Fork 1
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
Reactjs todo dv new #14
base: reactjs-todo-dv
Are you sure you want to change the base?
Reactjs todo dv new #14
Conversation
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.
So far, this is looking pretty good. I left some comments, but will zero-in on the davinci-flow.js
file as that's a really important component to get right.
renderForm(node); | ||
}; | ||
|
||
return ( |
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.
May I ask why the spinner component was removed?
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 was thinking the flow buttons might actually be links at some point but it can be added back. im still trying to get my head around how the sdk behaves vs what the flow is built like in DaVinci. This OOTB flow uses buttons.
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'm not super educated on DaVinci flows, but with my understanding, I wouldn't think these "flow buttons" would ever be links as they don't result in a navigation (a change in URL), but an internal change of reference of the form to render.
Were you thinking that click of a flow button would result in a navigation change to a new page/view for rendering the new flow? Or, that the internal form itself just renders different form elements?
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 was looking at the example app in javascript sdk repo and it was using links rather than buttons, that was the only reason.
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.
Oh, sorry for the confusion. I may have called them links, but technically, they are buttons. The are links in just the visual sense: https://github.com/ForgeRock/forgerock-javascript-sdk/blob/develop/e2e/davinci-app/components/flow-link.ts#L12.
return ( | ||
<BrowserRouter> | ||
<Routes> | ||
<Route path="login" element={<Login />} /> |
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 noticed that the register
path was removed from the routing. Can we preserve this feature, and have /register
go straight to a register flow, rather than login and then having to click on register again?
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.
technically we could go onto the next node using the sdk if the user clicked on the register button but that requires adding code based on a specific flow and impacting re-usability. It would be best to have separate flows, 1 for login, 1 for registration imo but something we could consider building into the example.
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'd suggest that if the "sign up" (register) link is clicked, we explicitly call the register flow through acr_values
. That way, we immediately load up the flow that the user selected. Thoughts?
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.
There is only 1 flow in the current OOTB DaVinci environments that encompasses both login and register. Might be easier to talk about this one on a call.
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.
Okay, if it's more complicated, we can set this aside when we both have a moment to chat. So, don't focus on this atm.
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 want to take a bit more time reviewing this file in particular. This is probably the most important and most difficult to write, so I'd like to make sure we get it as good as we can.
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.
100% ... this first pass was based on examples and limited exposure to the sdk which is not feature complete yet. plenty of room to iterate for sure.
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.
Okay, this is a review of just the davinci-flow.js
file. There's a lot of suggestions here, and if any are confusing or not articulated well, just ping us in one of our channels. Ryan or I can help out.
As a reference, I'd like to suggest a closer look at our form.js
file seen here: https://github.com/ForgeRock/sdk-sample-apps/blob/main/reactjs-todo/client/components/journey/form.js. It's by no means perfect, but many of the stated principles here are mostly implemented in that file.
Thanks!
* @function DaVinciFlow - React view for a DaVinci flow | ||
* @returns {Object} - React component object | ||
*/ | ||
export default function DaVinciFlow({ config }) { |
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.
From a component level, here are the principles that our team follows when it comes to React:
- State, both global and local, should be pure data, and not contain rendering elements/components
- Rendering is the output of state change; the old f(x) Functional Principle
- State logic should be encapsulated in a React hook
- React components contain only rendering logic (if state value is X render Y)
- React's
ref
should be avoided, if possible; limit `ref' to "referencing" live DOM elements
const [components, setComponents] = useState([]); | ||
const [pageHeader, setPageHeader] = useState(''); |
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'd recommend that these are removed from "state" and are just conditional rendered. This relates to #1 from the summary. I usually reserve using useState
for managing asynchronous state that needs to trigger a component rerender.
|
||
let client = useRef(null); | ||
|
||
const renderComplete = async (successNode) => { |
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.
Let's remove the word "render" from functions as they don't technically render anything.
const code = successNode.client?.authorization?.code || ''; | ||
const state = successNode.client?.authorization?.state || ''; | ||
await TokenManager.getTokens({ query: { code, state } }); | ||
const user = await UserManager.getCurrentUser(); | ||
methods.setUser(user.preferred_username); | ||
methods.setEmail(user.email); | ||
methods.setAuthentication(true); | ||
navigate('/'); |
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 all of this can stay in a function, but I'd place this within a useEffect
function and only have it run of we've reached success from a DaVinci flow. So, it's dependency should be the existence of auth code or something.
const renderError = (errorNode) => { | ||
const errorMessage = <ErrorMessage message={errorNode.error.message} key="error-message" />; | ||
setComponents([errorMessage]); | ||
}; | ||
|
||
// Represents the main render function for app | ||
async function renderForm(nextNode) { |
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'd just remove these two functions and place their contents in conditionals, rather than functions.
if (collector.type === 'TextCollector' && collector.name === 'protectsdk') { | ||
arrFormFields.push( | ||
<Protect | ||
collector={collector} | ||
updater={client.current.update(collector)} | ||
key={`protect-${collector.output.key}`} | ||
/>, | ||
); | ||
} else if (collector.type === 'TextCollector') { | ||
arrFormFields.push( | ||
<TextInput | ||
collector={collector} | ||
updater={client.current.update(collector)} | ||
key={`text-${collector.output.key}`} | ||
/>, | ||
); | ||
} else if (collector.type === 'PasswordCollector') { | ||
arrFormFields.push( | ||
<Password | ||
collector={collector} | ||
updater={client.current.update(collector)} | ||
key={`password-${collector.output.key}`} | ||
/>, | ||
); | ||
} else if (collector.type === 'SubmitCollector') { | ||
arrFormFields.push( | ||
<SubmitButton collector={collector} key={`submit-btn-${collector.output.key}`} />, | ||
); | ||
} else if (collector.type === 'FlowCollector') { | ||
arrFormFields.push( | ||
<FlowButton | ||
collector={collector} | ||
key={`flow-btn-${collector.output.key}`} | ||
flow={client.current.flow({ action: collector.output.key })} | ||
renderForm={renderForm} | ||
/>, | ||
); | ||
} |
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 do feel that this conditional logic can live in a function that's called on each item of an Array.map
callback. In the existing React app, we have it as mapCallbacksToComponents
.
// Used for redirection after success | ||
const navigate = useNavigate(); | ||
|
||
let client = useRef(null); |
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.
Let's see if we can remove the need for this useRef
. To do that, let's move the davinci
initialization out of the this component as it's a bit too "volatile", due to it's multiple rerenders. If we move it to a more stable component, and either add it as a React Context/Provider, or pass it down as an argument through the components.
const code = successNode.client?.authorization?.code || ''; | ||
const state = successNode.client?.authorization?.state || ''; |
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.
Even though this is technically valid, let's encourage the use of the getClient
"selectors" to grab more specific data. This results in better types and better null
exception handling.
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.
Hi Justin, can you give me an example here. I'm not familiar getClient selectors. thanks.
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.
Hey Jon, no worries! getClient
is just a method on the DaVinci client instance. It returns the client
portion of the current "node" data of the flow. You can see it being used here: https://github.com/cerebrl/davinci-client-demo/blob/main/main.ts#L35.
That's not to say what you did is wrong, but using the getClient
"selector" just provides some conveniences over the directly object lookups.
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.
thanks! guess I was on an older version of the client that didnt have that method. all set now.
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.
Oh, yeah, that is definitely true. These "selectors" have been recently added.
collectors.forEach((collector) => { | ||
if (collector.type === 'TextCollector' && collector.name === 'protectsdk') { | ||
arrFormFields.push( |
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 combination of Array.forEach
and the Array.push
within it is bit too "mutation driven". If we did something like this, I'd encourage the use of Array.map
that returns a new array, rather than mutating an existing array with forEach
. I hope that makes sense. When I'm using React, I think the more Functional Programming approach with its principle of immutability is best.
if (newNode.status === 'next') { | ||
renderForm(newNode); | ||
} else if (newNode.status === 'success') { | ||
return renderComplete(newNode); | ||
} else if (newNode.status === 'error') { | ||
return renderError(newNode); | ||
} else { | ||
console.error('Unknown node status', newNode); | ||
} |
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.
To avoid having to redeclare this conditional logic multiple times, let's move this down into the return statement. To ensure this component rerenders, we use the fact that the async-await
function above completes and new state is introduced to the component, either through a setState
or from a custom React Hook, and that's what triggers a rerender and this conditional logic is rerun in the return statement.
refactor updates
updates from feedback
* move dv client in context * move dv client in context
Signed-off-by: jon-oblander <[email protected]>
const node = await davinciClient.start(); | ||
|
||
if (node.status !== 'success') { | ||
renderForm(node); |
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.
rename this?
} | ||
|
||
// Update the UI with the new node | ||
async function renderForm(nextNode) { |
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 feels like a component, but its not.
Maybe this should be a custom hook?
} | ||
} | ||
|
||
function mapRenderer(nextNode) { |
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.
Same as above
{errorMessage.length > 0 && <ErrorMessage message={errorMessage} />} | ||
{errorMessage.length == 0 && | ||
collectors.map((collector) => { | ||
if (collector.type === 'TextCollector' && collector.name === 'protectsdk') { |
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 this map
callback should be abstracted, so its more clear to the reader what is happening in an isolated area.
return ( | ||
<Password | ||
collector={collector} | ||
updater={davinciClient.update(collector)} |
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.
Are we updating the collector in the render? Should this be done within the component, perhaps also inside of a hook?
import React, { useContext } from 'react'; | ||
import { AppContext } from '../../global-state'; | ||
|
||
const Text = ({ collector, updater }) => { |
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 see inconsistencies with how we are defining components. We should stick to one style. either arrows or function across the board.
isAuthenticated, | ||
prefersDarkTheme, | ||
username, | ||
loginClient, |
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.
do we want the loginClient to be passed in here? Seems like its a bit out of place and can be injected at build time below.
@@ -9,18 +9,11 @@ | |||
*/ | |||
|
|||
import { Config, TokenStorage } from '@forgerock/javascript-sdk'; | |||
import davinciClient from '@forgerock/davinci-client'; |
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.
Not a note for this PR - but i'm wondering if we want to default export this. I typically prefer avoiding default exports to reduce problems on the consumer side. We can discuss this further but default exports i think arent good for tree shaking and code discoverability.
methods.setAuthentication(true); | ||
// This function is passed to DaVinciFlow to be called when the flow is complete | ||
// Each individual flow might have a different completion action | ||
function onComplete() { |
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.
Is this a Component definition, inside of the Login
component definition?
baseUrl: AM_URL, | ||
timeout: '5000', | ||
baseUrl: BASE_URL, | ||
wellknown: `${BASE_URL}as/.well-known/openid-configuration`, |
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.
Should we be passing in just the base url or would it be better for the .env file to accept the whole openid configuration url?
No description provided.