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

I edited the initial post in this thread to reflect that this article has been updated to use react-basic-hooks. It also describes some new conveniences with create-react-app from @andys8 for bootstrapping hybrid PureScript / JavaScript projects. If you haven’t read the original or you’re interested in a refresher the post is updated now!

4 Likes

What is this Data.Interpolate magic?! :exploding_head:

Should these names be reversed?

+mkCounter :: Props -> JSX
+mkCounter = unsafePerformEffect counter
+
 counter :: Component Props
+import { mkCounter as Counter } from "./MyApp/Components/Counter.purs";
1 Like

It feels more natural to me to import counter when using this in PureScript code and for something like mkCounter to exist when exporting this via the FFI. But I’m open to changing my mind if I’m missing something here! I’ve only toyed with react-basic-hooks as we use the low-level bindings to react at work.

1 Like

Nate came up with this idea here: Feature request: String interpolation

It’s a neat trick where instance chains recursively act like Semigroup appends over the first argument.

class TCName a where
  functionName :: SemigroupLikeType -> a

instance first :: TCName SemigroupLikeType where
  functionName :: SemigroupLikeType -> SemigroupLikeType
  functionName value = value
else instance second :: TCName a => TCName (SemigroupLikeType -> a) where
  functionName :: SemigroupLikeType -> (SemigroupLikeType -> a)
  functionName firstSemi = 
    \secondSemi -> functionName (firstSemi <> secondSemi)
else instance third :: (OtherConstraintsNeeded b, TCName a) => TCName (b -> a) where
  functionName :: SemigroupLikeType -> (b -> a)
  functionName firstSemi = 
    \b -> functionName (funcFromOtherConstraints firstSemi b)
1 Like

Ah, ok. It’s very much up to you :slight_smile: I just started using that convention when the exports started becoming effects instead of the components themselves. That way they’d get imported as mkX instead of x, then used as x <- mkX as the parent component is constructed, and its eventual use in the render function would remain x. We also sometimes continue to export components using unsafePerformEffect do component ... and in those cases continue using the x name as the export.

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