[Updated]: How to replace React components using PureScript's React libraries

I missed that transition (when making components became effectful) – does that mean that you now need to do this when using them? (In that case my article is actually a bit incorrect and I should make an update :slight_smile: )

component = ...
  child1 <- mkChild1
  child2 <- mkChild2
  
  pure $ div_ [ child1, child2 ]

It’s usually like this: Root.purs (though this uses a custom monad instead of Effect, same thing though)

-hooks has always used Effect, but -classic and -compat never have, which can result in unexpected bugs when those older component creation functions aren’t used at the module level (unexpected remounts/rerenders, dropped component state, etc). Halogen uses slots and static typing to avoid this I believe, but internally React uses function instance reference equality (plus a key prop if it exists`) to determine component identity. Since JS isn’t statically typed there’s no safe way to reuse the old component state or props if the component function changes.

1 Like

Oh, React. Interestingly, I haven’t experienced those issues with the low-level bindngs, but I don’t believe we ever define more than one component per module or anything like that.

I’ll go ahead and update the names so that mkCounter is the default and perhaps jsCounter is the export (which can then be moved to the interop module).

I’m also (at @milesfrain’s suggestion) going to be moving the site content out into a dedicated repository where others can make suggestions to update the content for things like this, if you have more suggestions afterwards.

You will if you try to parameterize a component with a type class.

1 Like

Same, we started trying to make generic form components using typeclasses and found they would remount (clearing the form and DOM state) on every render :frowning:

1 Like

@thomashoneyman Section 3 (installing craco) - you also need to install the craco-purescript-loader. Thanks for updating this article! It’s nice to come back and work through it again.

2 Likes

Thanks for catching this! I’ve updated that – and I’ve also made my writing available on GitHub if anyone happens to notice other issues! Happy to make further updates to this article so that it represents PureScript and react-basic-hooks correctly. @spicydonuts I’ve also updated the article to change the use of mkCounter.

4 Likes

This is a great article, thank you @thomashoneyman !

I’m about to embark on a giant TypeScript-to-PureScript conversion project. My own experience, the experience of my team, and your very compelling article has convinced me that this is a good idea and it’ll work.

The only thing I’m slightly apprehensive about is the react-redux in the TypeScript code. I’ve never worked with redux before.

If I’m reading this correctly, you were referencing an article about Sharing more complex information like Redux stores here? I can’t find that article, does it exist?

This post demonstrates how to take over a React component using PureScript. It’s meant for beginner-to-intermediate PureScript devs and omits no code along the way. It’s the first of three articles:

  1. Taking over individual components with PureScript React & React Basic
  2. Doing the same with Halogen
  3. Sharing more complex information like Redux stores

More generally, do you have any advice on how to deal with react-redux in a PureScript conversion project?

1 Like

Taking over an entire app with PureScript is quite a different beast. The challenges vary depending on the architecture of the existing app. I had to do this with my current project, which started as an inherited mess of TypeScript, React and Redux…

We had been expressly told not to “rewrite from scratch” but 3 months into development, and while adding new features, there was no TypeScript or Redux left. So it wasn’t from scratch, but it was a rewrite.

1 Like

Alas, I should never post my writing plans, as my ambition outpaces my available time. I haven’t written the other articles (yet). I probably won’t end up getting to the Halogen one now that it’s been years since I’ve done this, but I could still write about sharing a Redux store between JS React and PureScript React.

It is possible to share a Redux store and middleware, but as @robertdp mentioned, this is more involved than sharing components would be especially if your Redux setup is hairy. I do hope to come back to this topic, perhaps over this winter break, but I can’t promise I’ll have the time.

@natefaubion originally wrote the code to deal with Redux when converting our app, so he may have advice to share in this thread. I can hopefully circle back to this thread when I have some time to share my understanding as well. Sorry I don’t have more for you right now!

3 Likes

I can try to give a quick rundown of the strategy I used, with some vague hand-waving. It really varies from project to project, and with Redux this can be further complicated by extra “extension” libraries and how the data flows to and from the store.

The basic idea is that the process needs to be top-down, and was something like:

  1. Identify the global state in the legacy project.
  2. Investigate how this state is used. This gives you an idea of what happens when you externalise control of the state.
  3. Build an interface that lets you inject state into the store.
  4. Set up rendering of the old app inside your new one.
  5. Do what you need to push state to the old app, and start the process of remaking top-level components in PureScript.

My recollection is something like:

  1. For us this was really just some authentication and user state in Redux, and routing provided by React Router.
  2. Our legacy store was really dumb (only pure updates to the state), which should have made it simpler, but there was also some really bizarre “factory” helpers that read values from the store instance directly. I ended up crippling the auth reducer and turning it into something that just received the auth state and put it straight into the store.
    React Router was also pretty annoying, and we ended up leaving it in place for fear of breaking things.
  3. Just a bunch of mechanical reducer updates. Not elegant at all, but worked like a charm.
  4. We just had a LegacyApp route that rendered the old app. We ran into an issue at the start where we were rendering our new PS layout with the legacy app inside, still using its version of the layout. If your old layout is dumb then it’s better to remove it and use the PS version if you can.
  5. This is where the ball started rolling. Issues did pop up along the way, usually with requirements changing for legacy pages, but it was always easier to just remake the pages in PS than it was to update them in the legacy app.

Update: I feel like I have to add that our legacy app was inherited from a previous failed project, and that we were required to start with it. I don’t know anything about the previous project except that it was really poorly made, and that apparently fixing state/logic bugs took weeks per bug.

Features took months…

4 Likes

Our experience is similar to @robertdp for a hybrid JS/PureScript Redux app.

  • Completely separate code managed by PureScript and code manage by JS. But this, I mean do not write a feature such that JS and PS reduce over the same state.
  • Do not write PS code that depends on particularities of your Redux stack. That is, don’t try to reason about dynamic shapes of messages that various middleware might look at or whatever. My experience is that the JS Redux middleware patterns kind of suck, and you will have a really bad time trying to work with it from PS. Just pretend it doesn’t exist.
  • Write a middleware for routing PS effects and reducers. We use Variant for actions, and Run (with ListT-like semantics) for effects. PS is good at handling effect abstraction, and it’s better than any chain of middleware you can come up with in JS. We box them up in a tagged message and have a small middleware that dispatches these messages to PS code. It’s unsafe, but it’s at the root of the application so unlikely to go wrong or undergo any churn once it’s working.
  • We write Interop modules when things needs to be consumed across language boundaries. For example, if JS needs to consume a PS component, the JS imports the Interop module instead of the PS module directly. This is just to have a canary for when someone changes the types of the PS component, and we have something that says “go check and verify any JS code that imports this”.
2 Likes

@natefaubion Does this mean that you kept Redux around in the PS version as well? Our legacy app seemed to be using it without any clear reason or goal, so we let it disappear along with the old code.

1 Like

Yes, we kept Redux around. We definitely have a need for some kind of state management, and also a need to interop between JS and PS, so we opted to use Redux as a central bus. At some point we will probably swap it out once the JS code disappears.

1 Like

Makes sense. Most of our state was page-level with no statefulness requirement, so I built specialised “manager” components for the few bits that were actually global. Nothing big or complex, and it makes the data architecture and logic really obvious.

And our requirements for getting, using and updating each piece of global state was very different, so separating them seemed best.

1 Like

React Router was also pretty annoying, and we ended up leaving it in place for fear of breaking things.

@robertdp Did you figure out how to write withRouter in PureScript? Or how to wrap withRouter in PureScript? I want to leave react-router in place too, but then how do I history.push() in PureScript?

1 Like

I mounted the legacy app inside the new PureScript app, so the new routing took precedence. The legacy app then just used react-router normally, but I also needed to update the hash to trigger the popstate event in the PS component.

history.push('/logout');
window.location.hash = "refresh";

It’s not a great solution, but it worked okay for the few months we used it.

Update: I’ve just been looking through old commits, and found this as well:

unsafeTriggerPopstate :: Effect Unit	
unsafeTriggerPopstate = do	
  Hash.setHash "refresh"	
  Hash.setHash ""

This was set to run every time the legacy app was routed to, to trigger the popstate event for react-router.

void $ Timer.setTimeout 100 Routing.unsafeTriggerPopstate -- to wake up the legacy router
3 Likes

For anyone doing it the other way, with PureScript components routed by the legacy app, I made this:

1 Like

Not sure if this is the right place to ask this, but my use case is a little different: I’m about to start a new web application from scratch and would like to use Purescript. I’m leaning towards using React, in order to benefit from the massive number of prebuilt components around. However, I didn’t find good resources on integrating such components into Purescript.

I know about the FFI, of course, but learning Purescript, plus learning React in Purescript, you get confused about how to properly add types to code that someone else has written. The given article explains some of the work involved, I’m not sure I feel confident I’m able to adapt this to my use case. For instance, say I’d want to use the Autocomplete component of the giant material-ui library from Purescript. How would I even approach that?

I’d also be open to writing the app in Halogen even, but I’d still like to be able to integrate React components.

Any pointers?

1 Like

This is a example how I use the Link component from Next.js in a PureScript project.

This is tiny compared to something like AutoComplete, but the general approach is the same.

3 Likes