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

Sync state with URL (Share function) #117

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open

Conversation

mreishus
Copy link
Contributor

@mreishus mreishus commented Nov 13, 2023

Obviously there is great demand for this function since there are 3 unmerged PRs with this.

I took the ideas for #107 and

  • pushed out the responsibility for each reducer to determine how it will serialize and serialize.
  • Allowed for partial state updates: Each reducer can choose to only url serialize some important keys.

Note, I did use the same centralized workaround for selecting state.request.endpoint on initial load - since that is too big to store in a URL. I couldn't figure out how to push this to one reducer.

This one is pretty verbose, with lots of comments and some long variable names, but I wanted to make it easy to understand.. although sometimes I realize being too verbose can have the opposite effect.

@mreishus mreishus self-assigned this Nov 13, 2023
@mreishus mreishus requested review from dmsnell and yansern November 13, 2023 23:40
Copy link
Member

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again @mreishus for this contribution!

This is a very ambitious PR, and in the comments I'm even asking for things to make it more ambitious.

That being said I wonder if it's possible to break it apart into smaller independent pieces that raise fewer design questions.

  • What if we started by providing a way to paste in a JSON document describing the current request? That seems like it would easier in some ways to follow the URL if we have one encoded parameter than many.
  • Parsing URL parameters into app state seems a bit fragile and adds coupling that I don't think needs to exist. Parsing URL parameters into a Redux action seems like it would not only remove that coupling, but make it easier to ensure that back-button behavior remains consistent with the app.
  • We're trying to fit all the URL parameters into the different parts of the request, but it really feels like what we're doing is not mangling app state, but performing a new semantic action not currently defined in the app: something like LOAD_REQUEST or SET_REQUEST. If we create that then I think much of the logic in this PR would disappear.

So I wonder if we could start small by creating that new action, that new semantic intent. That would let us do interesting things we currently can't, such as set a request from the console with a JSON body representing the request. That action could fit in with any other necessary work required to handle app loading, and in fact, maybe adding state-tracking for isFetchingEndpoints would be a preparatory PR that enhanced the UI while paving the way for SET_REQUEST.

Beyond this it would be nice to see consistent use of JSDoc, which is so widely supported in various IDEs and other tools. I'm curious what some of the motivations are for various optimizations here, as there's a sense in which they seem premature.

Thoughts?

// state at launch by merging saved states and any state that's encoded in
// the URL. This is important when the URL provides partial state updates,
// which must be combined with existing state without loss of detail.
let urlParams = new URL( window.location.href ).searchParams;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not important, but we can also create these directly with:

const urlParams = new URLSearchParams( window.location.search );

import { getEndpoints } from '../../state/endpoints/selectors';
import { loadEndpoints } from '../../state/endpoints/actions';

/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opening up with /** carries the specific meaning of starting a JSDoc block, but this is not attached to any node.

it would probably be better here to use /* and */ instead of doubling the stars, unless there's some standard I'm unaware of. if we want to emphasize the comment we can add additional visual fluff inside of it.

I couldn't find any official clarity on this, but if we want this to be a module-level docblock, maybe we should put it above the import statements?

// They did send an endpointPath in the URL. In order to fill the entire
// endpoint state, we need to load all endpoints, then we can find a match after load.
const { dispatch } = store;
loadEndpoints( apiFromUrl, versionFromUrl )( dispatch );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetching happens automatically anyway doesn't it?
and should we be loading these also in the same way with logged-out views?
seems like isInitializing is a property that would be appropriate to handle in the core app and not in this URL middleware, for example, to make the loading display of the app better.

// endpoint state, we need to load all endpoints, then we can find a match after load.
const { dispatch } = store;
loadEndpoints( apiFromUrl, versionFromUrl )( dispatch );
return { isInitializing: true, endpointPathLabeledInURL };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isInitializing feels a bit generic for a very specific thing here. although I think there's value in rethinking where and how this is stored, in this particular case something like isFetchingEndpoints seems clearer if debugging or trying to repurpose it.

};

// This middleware is responsible for serializing the state to the URL.
// It also handles a special case of loading endpoints and setting the selected endpoint.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick, but if we embrace JSDoc syntax here then IDEs will use these comments to show in-place documentation when calling this function, and if we add type annotations in the @param tags then most IDEs now will highlight type errors without configuring anything and without installing TypeScript

const result = next( action );

// Serialize and upate the URL.
if ( actionsThatUpdateUrl.includes( action.type ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we include the actions here directly in a switch statement it will mirror all of the other Redux reducers and middleware, and remove the indirection the array with .includes() introduces

};


export default serializeToUrlMiddleware;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected to see a listener for popstate in this middleware file but I don't see one. are we updating the app state when someone hits the back button?


// Let each reducer handle its own state
const deserializedState = reducers( paramsObject, { type: DESERIALIZE_URL } );
return deserializedState;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to convert the URL parameters into Redux actions instead of app state?

if that were the case it would seem more reusable so listen for popstate events and update the app using the same mechanisms that were used to set it up originally.

alternatively, we could also create a new action that sets all parameters while clearing out the old ones. this could be used in those history events and remove the need for the deepMerge logic, which seems a bit fragile, and spread out the reducer logic into these separate places.

endpointPathLabeledInURL: 'epu',
version: 've',
api: 'ap',
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this necessary? if we expect big URLs, then this seems like a rather minor optimization which brings a fairly big reduction in readability. if we don't expect big URLs, do we need to shorten them?

@mreishus
Copy link
Contributor Author

mreishus commented Nov 20, 2023

We're trying to fit all the URL parameters into the different parts of the request, but it really feels like what we're doing is not mangling app state, but performing a new semantic action not currently defined in the app: something like LOAD_REQUEST or SET_REQUEST. If we create that then I think much of the logic in this PR would disappear.

So I wonder if we could start small by creating that new action, that new semantic intent. That would let us do interesting things we currently can't, such as set a request from the console with a JSON body representing the request. That action could fit in with any other necessary work required to handle app loading, and in fact, maybe adding state-tracking for isFetchingEndpoints would be a preparatory PR that enhanced the UI while paving the way for SET_REQUEST.

I can imagine a LOAD_REQUEST - basically, it's the same as DESERIALIZE_URL in this PR, except perhaps the serialized information is coming from a field on the action instead of the previous state. However, what's the other half of this? How will we take the current state and serialize it into something that can be loaded into LOAD_REQUEST in a generalized way?

@dmsnell
Copy link
Member

dmsnell commented Nov 20, 2023

How will we take the current state and serialize it into something that can be loaded into LOAD_REQUEST in a generalized way?

This sounds like making a new selector and doing largely the same thing you did here, but maybe instead of serializing into a URL we could start by codifying the properties in a JSON payload that could be used on LOAD_REQUEST.

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

Successfully merging this pull request may close these issues.

2 participants