3

Observables to Render React Components

 1 year ago
source link: https://blog.bitsrc.io/observables-to-render-react-components-b5d1fd339734
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Observables to Render React Components

A faster, more organised and overall better way to render components

What is the most elegant way you manage the state of your react components? It has to be maintainable, easy to implement and performant.

0*hEMP6uDnAKFZBfzp
Photo by Caleb George on Unsplash

The following pattern has been a work in progress for the last year and I thought now was a good time to share it with you. It started as a POC using streams to render React components, but using large stream libraries wasn’t really worth it. Observables turned out to be a good alternative.

Why observables? They are a simple feature, which is currently proposed to become part of the JavaScript language. https://github.com/tc39/proposal-observable.

React state should be isolated by domain and components should be re-rendered in a targeted way; only those components that use the data should be updated and no others. We do not want to do data comparisons or change checks or anything that impairs performance.

We do that by dividing our data sources up into a service layer. We will have a different service for each. type of data. For example, we can have a UserService that updates and retrieves user data or a WeatherService that updates information about the weather. Each data type should have a separate service. A blogging website might have an ArticleService for example.

A service in this paradigm returns an <<Observable>> when we retrieve data, and a regular synchronous operation when we mutate data. This way we make sure that we have a single source of truth.

For example, TypeScript method types would look something like this:

class UserService {
getUser(id:string):Observable<UserDTO | Error> {}
updateUser(user:UserDto):void {}
}

When we want to retrieve a user, we get an observable back, which will resolve one or more user data objects (UserDTOs). Why one OR more data objects? It is an observable, which is made to resolve once or more times, until we unsubscribe. Whenever the method updateUser gets called, a new updated version will be pushed through the observable.

1*QRpdhD9BQLd1l8ZMFfNSAQ.png

It is possible to make calls to our service layer directly from our view component using a useEffect hook, but it would be more elegant to use a custom hook for this. We can get a decent separation of concerns; a view layer, hooks as controllers, and concrete services as a service layer. Services and views can easily be combined, we have strongly decoupled components while maintaining cohesion.

This architecture complies with the following architectural principles:

  • Services and views are decoupled
  • With high cohesion, it is simple to see how everything works and data flows. It is simpler than reducers, for example, which are highly decoupled as well but lack cohesion.
  • Descriptive modules; it is easy to identify what each component does.
  • Extendable, it is easy to add functionality to services without bloating views or hooks. Business logic is added there where it belongs.
  • Data can be shared between components, something a simple fetch hook can’t do.
  • Any backend can be used, this pattern leads itself to WebSockets, HTTP requests and even localStorage.
1*ut9eBIxWB7AyNLAbVTq_uQ.png

Show me the code

So what does the code look like? See an example user-service below.

class UserService {constructor() {
this.observers = [];
this.state = {};
this.observable = new Observable((observer) => {
this.observers.push(observer);
observer.next(this.state);
return () => {
this.observers = this.observers.filter((obs) => obs !== observer);
};
});
}
write(state): void {
this.state = state;
this.observers.forEach((observer) => {
if (observer && observer.next) observer.next(state);
});
}getUser(): Observable<ServiceStatus> {
const shouldUpdate = !this.observers.length;
if (shouldUpdate) {
this.write({ ...this.state, status: Status.Loading });
this.repo.getUser()
.then((user) => {
this.write({
...this.state,
status: Status.Success,
user,
})
})
.catch((e) => {
this.write({
error: e.message,
status: Status.Error,
user: undefined,
});
});
}
return this.observable;
}updateUser(data){
this.write({
...this.state,
status: Status.Success,
user,
});
await this.repo.updateUser()
// handle errors and roll back the updated user data on failure
}
}

In this class we can see we have two methods, one to create a subscription to an observable and one to update our local state. The setup we have here is very flexible, and we can implement any additional method or feature as we need. You might want to add functionality like state expiration, state rollback and optimistic updates. It would be a good idea to create these extras using mixins.

Example hook

The code below shows an example hook that uses the user service. It would be simple to make this hook generic and reusable for any observable service. It is best to let the individual developer decide how to organise this.

const useUserService = (userService) => {
const [user, setUser] = useState({});
const [status, setStatus] = useState(Status.Loading);
const hasError = useRef(false);

useEffect(() => {
const observable = userService.getUser();
const subscription = observable.subscribe((state) => {
if (state.status === Status.Error)
setUser(null);
setStatus(Status.Error);
hasError.current = true;
} else if (state.Status === Status.Success)
setUser(state.user);
setStatus(Status.Success);
hasError.current = false;
}
});
return () => {
subscription.unsubscribe();
};
}, []);
return { user, status, update};
};

This hook is relatively simple as well. It provides the glue between the React component below and the service.

Finally, we have our view component, which should be easy enough.

const UserView = (props) => {
const { user, status, update} = useUserService();
if(status.LOADING){
return <LoadingIndicator />
}
return (
<article>
<div>{user}</div>
<button onClick={()=>update(user)} >Save</button>
</article>
);
};

We can use the hook on multiple places in an application and they will all receive the same data, even when the user is updated. Magic.

I think this is quite a neat way of managing application state. Maybe it takes a little more code than some other methods, but in exchange, you get a clear data flow and a framework that is very flexible and extendable.

Thank you for reading, I hope you found this article interesting. Please support me by following me here on Medium or on Twitter.

Build apps with reusable components like Lego

1*mutURvkHDCCgCzhHe-lC5Q.png

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK