DEV Community

loading...
Play Button Pause Button

Refactor to Refract

xendke profile image Juan Xavier Gomez ・3 min read

Reactive Programming is hard

In 2018, I worked on a project that used RxJS to create some Observables for api calls. The code looked complicated, RxJS itself was complicated, and I had a hard time working with it.
I don't think I really understood what reactive programming was about. Overall, I disliked the concept, or to be more truthful, I found it hard to understand and that lead to frustration.

Practice makes perfect

I started working at Fanduel about a year ago. Since then I've been delving more and more into reactive programming using the library refract originally developed by folks at the company.

Refract describes itself as a way to:

Handle your component effects and side-effects in a clear and declarative fashion by using asynchronous data streams

The library was hard to understand at first, and the benefits of it were not clear to me back then. To me, it simply seemed like an alternative to React.useEffect.

Nowadays I feel more comfortable using reactive programming, streams, and refract. So today I decided to spend one hour refactoring some useEffects into a withEffects aperture just to test my knowledge and experience. After this session (and a year of experience), the benefits of this approach have become more clear to me.

Below I have the original logic that used useEffects. For context, this logic comes from a Profile Page component that displays some user information (other users or your own) and all of the user's posts.

Before:

useEffect(() => {
  if (user.info && !params.userId) setUserData(user.info)
}, [user.info, params.userId])

useEffect(() => {
  if (!params.userId) return
  firebase.doUserInfoGet(params.userId).then((res) => {
    setUserData(res.data())
  })
}, [params.userId, firebase])

useEffect(() => {
  const iife = async () => {
    const postsCollection = await firebase.doUserPostsGet(
      params.userId || user.auth.uid
    )
    const newPosts = []

    postsCollection.forEach((post) => {
      newPosts.push(post.data())
    })
    setPosts(newPosts)
    setLoadingPosts(false)
  }
  iife()
}, [user.auth, params.userId, firebase])
Enter fullscreen mode Exit fullscreen mode
View more on GitHub.

The first effect decides whether or not to use your own information for the profile page according to the params coming from the url.
The second effect makes the api call for other user's information.
The third effect makes the api call for the user's posts.

My useEffects worked fine. They definitely could have used improvements on their own. But nonetheless, they were hard to understand.
My biggest concern was that when I came back to this codebase after 6 months I could not recognize:

  • why the effects needed to execute &
  • when they executed

Using refract I was able to abstract and clean up the logic that was needed by the component.

Observable streams are hard to understand at first, but once you get past that hurdle, you'll soon realize that they allow you to more easily organize effects. This is thanks to the api provided by refract and xstream. Reading the effect streams should make it easier understand why the effects are needed and when each effect should take place.

After:

const aperture = (component, { firebase, user }) => {
  const userIdParam = () =>
    component.observe('match', ({ params }) => params.userId)

  const loadOtherUserInfo$ = userIdParam()
    .filter((param) => param)
    .map((userId) =>
      xs.fromPromise(
        firebase
          .doUserInfoGet(userId)
          .then((res) => res.data())
          .catch(() => ({ error: true }))
      )
    )
    .flatten()
    .compose(
      sampleCombine(component.observe('match', ({ params }) => params.userId))
    )
    .map(([userData, userId]) => ({ userData, isOwnProfile: false, userId }))

  const useOwnUserInfo$ = userIdParam()
    .filter((param) => !param)
    .mapTo({
      userData: user.info,
      isOwnProfile: true,
      userId: user.auth && user.auth.uid,
    })

  const loadPosts$ = userIdParam()
    .map((param) => param || user.auth.uid)
    .map((userId) => xs.fromPromise(firebase.doUserPostsGet(userId)))
    .flatten()
    .map((postsCollection) => {
      const posts = []
      postsCollection.forEach((post) => {
        posts.push(post.data())
      })
      return { loadingPosts: false, posts }
    })

  return xs
    .merge(useOwnUserInfo$, loadOtherUserInfo$, loadPosts$)
    .startWith({ posts: [], loadingPosts: true })
    .map(toProps)
}
Enter fullscreen mode Exit fullscreen mode
View more on GitHub.

The refract api definitely has a learning curve, but boilerplate aside, the effects are much more declarative and easier to come back to after a long hiatus!

TL;DR: Don't be afraid to refractor!

Discussion (0)

pic
Editor guide