Skip to content
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

Open
wants to merge 12 commits into
base: reactjs-todo-dv
Choose a base branch
from

Conversation

jon-oblander
Copy link

No description provided.

Copy link
Contributor

@cerebrl cerebrl left a 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 (
Copy link
Contributor

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?

Copy link
Author

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.

Copy link
Contributor

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?

Copy link
Author

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.

Copy link
Contributor

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 />} />
Copy link
Contributor

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?

Copy link
Author

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.

Copy link
Contributor

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?

Copy link
Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Author

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.

Copy link
Contributor

@cerebrl cerebrl left a 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 }) {
Copy link
Contributor

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:

  1. State, both global and local, should be pure data, and not contain rendering elements/components
  2. Rendering is the output of state change; the old f(x) Functional Principle
  3. State logic should be encapsulated in a React hook
  4. React components contain only rendering logic (if state value is X render Y)
  5. React's ref should be avoided, if possible; limit `ref' to "referencing" live DOM elements

Comment on lines 34 to 35
const [components, setComponents] = useState([]);
const [pageHeader, setPageHeader] = useState('');
Copy link
Contributor

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) => {
Copy link
Contributor

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.

Comment on lines 43 to 50
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('/');
Copy link
Contributor

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.

Comment on lines 53 to 59
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) {
Copy link
Contributor

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.

Comment on lines 69 to 106
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}
/>,
);
}
Copy link
Contributor

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);
Copy link
Contributor

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.

Comment on lines 43 to 44
const code = successNode.client?.authorization?.code || '';
const state = successNode.client?.authorization?.state || '';
Copy link
Contributor

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.

Copy link
Author

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.

Copy link
Contributor

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.

Copy link
Author

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.

Copy link
Contributor

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.

Comment on lines 68 to 70
collectors.forEach((collector) => {
if (collector.type === 'TextCollector' && collector.name === 'protectsdk') {
arrFormFields.push(
Copy link
Contributor

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.

Comment on lines 138 to 146
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);
}
Copy link
Contributor

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.

const node = await davinciClient.start();

if (node.status !== 'success') {
renderForm(node);
Copy link
Contributor

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) {
Copy link
Contributor

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) {
Copy link
Contributor

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') {
Copy link
Contributor

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)}
Copy link
Contributor

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 }) => {
Copy link
Contributor

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,
Copy link
Contributor

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';
Copy link
Contributor

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() {
Copy link
Contributor

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`,
Copy link
Contributor

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants