In modern online application development, particularly with frameworks like React, the singleton service is useful for managing state and providing shared functionality across an application.
A singleton service is an instance that is created once and reused throughout the lifespan of an application. This pattern effectively maintains global state, configuration settings, or any shared logic that numerous components need access to.
Using a singleton service in React, developers can ensure consistent and efficient resource management, minimize wasteful re-renders, and keep the architecture clean.
React Singleton Service Example: Singleton Pattern Using React development
Let’s look at a detailed example of implementing the Singleton Pattern in a React application with TypeScript.
In this example, we’ll define a singleton class for managing user authentication status and show how to use it in a React component.
// AuthManager.tsx class AuthManager { private static instance: AuthManager; private isAuthenticated: boolean = false; private constructor() {} public static getInstance(): AuthManager { if (!AuthManager.instance) { AuthManager.instance = new AuthManager(); } return AuthManager.instance; } public login(): void { this.isAuthenticated = true; } public logout(): void { this.isAuthenticated = false; } public isAuthenticatedUser(): boolean { return this.isAuthenticated; } } export default AuthManager;
In the code snippet:
We define the AuthManager class with a private property called isAuthenticated to keep track of the user’s authentication state.
The private constructor ensures that AuthManager instances may only be created within the class.
We use the getInstance() method to return a single instance of AuthManager.
The login() and logout() functions change the authentication state.
The isAuthenticatedUser() method determines if the user is authenticated.
React Component using AuthManager
// AuthComponent.tsx import React, { Component } from 'react'; import AuthManager from './AuthManager'; class AuthComponent extends Component { private authManager: AuthManager; constructor(props: {}) { super(props); this.authManager = AuthManager.getInstance(); } handleLogin = () => { this.authManager.login(); this.forceUpdate(); // Update component to reflect the change }; handleLogout = () => { this.authManager.logout(); this.forceUpdate(); // Update component to reflect the change }; render() { const isAuthenticated = this.authManager.isAuthenticatedUser(); return ( <div className="auth-component"> <h2>Authentication Example using Singleton Pattern</h2> <p>User is {isAuthenticated ? 'authenticated' : 'not authenticated'}</p> <button onClick={this.handleLogin}>Login</button> <button onClick={this.handleLogout}>Logout</button> </div> ); } } export default AuthComponent;
Here’s what’s occurring in the React component.
We import AuthManager to manage authentication using a Singleton instance.
We create an instance of AuthManager in the constructor by calling AuthManager.getInstance().
The handleLogin and handleLogout routines communicate with the Singleton instance to update the authentication status, which prompts a component update to reflect the change.
The render method displays the authentication status and includes buttons that replicate login and logout activities.
Putting It All Together
To demonstrate this example, import and use the AuthComponent within your main application file, such as App.tsx:
// App.tsx import React from 'react'; import AuthComponent from './AuthComponent'; function App() { return ( <div className="app"> <AuthComponent /> </div> ); }
export default App;
In this example, the Singleton Pattern is used to design and maintain a single AuthManager object that manages user authentication status. This instance is then used in a React component to show how the Singleton instance can be retrieved and used.
React Singleton Service Example Github
Let’s build a basic API service that retrieves data from a public API while ensuring that only one instance of this service is utilized throughout the application.
Define the Service Class:
javascript
// ApiService.js class ApiService { constructor() { if (ApiService.instance) { return ApiService.instance; } ApiService.instance = this; } async fetchData(url) { const response = await fetch(url); const data = await response.json(); return data; } } export default new ApiService();
In this example, the ApiService class includes a constructor that checks to see if an instance already exists. If it does, it returns the current instance. Otherwise, it launches a new instance and assigns it to ApiService.instance.
Use the Singleton Service in a Component:
javascript
// App.js import React, { useEffect, useState } from 'react'; import ApiService from './ApiService'; const App = () => { const [data, setData] = useState(null); useEffect(() => { const fetchData = async () => { const result = await ApiService.fetchData('https://jsonplaceholder.typicode.com/posts'); setData(result); }; fetchData(); }, []); return ( <div> <h1>Singleton Service Example</h1> {data ? ( <ul> {data.map((item) => ( <li key={item.id}>{item.title}</li> ))} </ul> ) : ( <p>Loading...</p> )} </div> ); }; export default App;
We import the ApiService in this component and use it to retrieve data from a public API when it is mounted. The useEffect hook ensures that data fetching occurs only once when the component is first rendered.
What is React Singleton Hook?
Singleton hooks are pretty similar to React Context in terms of functionality. Each singleton hook has a body, which you may think of as a Context Provider body. Hook has a return value that is similar to the one provided by context. Using a singleton hook from a component is similar to consuming a context.
Singleton hooks are sluggish. The body is not executed until the hook is triggered by another component or hook. Once loaded, the hook body stays loaded indefinitely. If you want to eager-load some Singleton hooks, utilize them in your App’s top-level component.
Singleton hooks do not require a provider or a specific App structure. It takes advantage of useState/useRef/useEffect, as well as certain lesser-known react features, to improve performance and portability.
Singleton hooks, the React-Redux hooks API, React Context hooks, and any custom hook can all be mixed into a single project.
React Singleton Hook Example
In the code below, the user profile is not fetched until useUserProfile is called by another component, and once fetched, it is never reloaded again; the hook is permanently mounted into a hidden component.
importĀ { useEffect, useState } from 'react'; import { singletonHook } from 'react-singleton-hook'; const api = { async getMe() { return { name: 'test' }; } }; const init = { loading: true }; const useUserProfileImpl = () => { const [profile, setProfile] = useState(init); useEffect(() => { api.getMe() .then(profile => setProfile({ profile })) .catch(error => setProfile({ error })); }, []); return profile; }; export const useUserProfile = singletonHook(init, useUserProfileImpl);
dark/light mode switch
Whenever Configurator changes darkMode, all subscribed components are updated.
/*************** file:src/services/darkMode.js ***************/ import { useState } from 'react'; import { singletonHook } from 'react-singleton-hook'; const initDarkMode = false; let globalSetMode = () => { throw new Error('you must useDarkMode before setting its state'); }; export const useDarkMode = singletonHook(initDarkMode, () => { const [mode, setMode] = useState(initDarkMode); globalSetMode = setMode; return mode; }); export const setDarkMode = mode => globalSetMode(mode); /*************** file:src/compoents/App.js ***************/ importĀ React from 'react'; import { useDarkMode, setDarkMode } from 'src/services/darkMode'; const Consumer1 = () => { const mode = useDarkMode(); return <div className={`is-dark-${mode}`}>Consumer1 - {`${mode}`}</div>; }; const Consumer2 = () => { const mode = useDarkMode(); return <div className={`is-dark-${mode}`}>Consumer2 - {`${mode}`}</div>; }; const Configurator = () => { const mode = useDarkMode(); return <button onClick={() => setDarkMode(!mode)}>Toggle dark/light</button>; }; imperatively read hook state for non-react code import { useState } from 'react'; import { singletonHook } from 'react-singleton-hook'; const initDarkMode = false; let currentMode = initDarkMode; let globalSetMode = () => { throw new Error(`you must useDarkMode before setting its state`); }; export const useDarkMode = singletonHook(initDarkMode, () => { const [mode, setMode] = useState(initDarkMode); globalSetMode = setMode; currentMode = mode; return mode; }); export const setDarkMode = mode => globalSetMode(mode); export const getDarkMode = () => currentMode;
use react-redux (or any other context) inside singletonHook
To use react-redux or any other context-based functionality, mount singleton hooks beneath the provider in your app. To achieve this, import SingletonHooksContainer from react-singleton-hook and place it wherever in your app.
SingletonHooksContainer must be rendered before any components that use a singleton hook!
By default, you do not need to deal with a SingletonHooksContainer; we execute this component internally in a separate react app.
/*************** file:src/services/currentUser.js ***************/ import { singletonHook } from 'react-singleton-hook'; import { useSelector } from 'react-redux'; const init = { loading: true }; const useCurrentUserImpl = () => { const session = useSelector(state => state.session); if (session.loading) return init; return session.user; }; export const useCurrentUser = singletonHook(init, useCurrentUserImpl); /*************** file:src/App.js ***************/ import React from 'react'; import ReactDOM from 'react-dom'; import { SingletonHooksContainer } from 'react-singleton-hook'; import { Provider } from 'react-redux'; import store from 'src/store'; import App from 'src/views'; const app = ( <Provider store={store}> <> <SingletonHooksContainer/> <App/> </> </Provider> ); ReactDOM.render(app, document.getElementById('root'));
top-level components updated before low-level components
/*************** file:src/services/session.js ***************/ import { useEffect, useState } from 'react'; import { singletonHook } from 'react-singleton-hook'; const initState = { loading: true }; let setSessionGlobal = () => { throw new Error('you must useSession before login'); }; const useSessionImpl = () => { const [session, setSession] = useState(initState); setSessionGlobal = setSession; useEffect(() => { setSession({ loggedIn: false }); }, []); return session; }; export const useSession = singletonHook(initState, useSessionImpl); export const login = (name, pass) => { setSessionGlobal({ loggedIn: true, user: { name: 'test' } }); }; /*************** file:src/index.js ***************/ import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; import { login, useSession } from 'src/services/session'; const LoggedInView = () => { const session = useSession(); console.log(`LoggerInView rendered with ${JSON.stringify(session)}`); return null; }; const LoggedOutView = () => { const session = useSession(); console.log(`LoggedOutView rendered with ${JSON.stringify(session)}`); return null; }; const WaitingForSessionView = () => { const session = useSession(); console.log(`WaitingForSessionView rendered with ${JSON.stringify(session)}`); return null; }; const MainComponent = () => { const session = useSession(); useEffect(() => { setTimeout(() => { login('testuser'); }, 2000); }, []); console.log(`MainComponent rendered with ${JSON.stringify(session)}`); if (session.loading) return <WaitingForSessionView/>; if (session.loggedIn) return <LoggedInView/>; return <LoggedOutView/>; }; ReactDOM.render(<MainComponent/>, document.getElementById('root')); /*************** console.log ***************/ /* MainComponent rendered with {"loading":true} WaitingForSessionView rendered with {"loading":true} MainComponent rendered with {"loggedIn":false} LoggedOutView rendered with {"loggedIn":false} MainComponent rendered with {"loggedIn":true,"user":{"name":"test"}} LoggerInView rendered with {"loggedIn":true,"user":{"name":"test"}} */
React Singleton Service Tutorial: How to Build a New React Application
First, let’s start a new React application using start React App.
bash
npx create-react-app react-singleton-service
cd react-singleton-service
npm start
Next, we’ll build a basic service to handle a counter-state. This service will provide methods for incrementing, decrementing, and obtaining the current value of the counter.
javascript
// src/services/CounterService.js class CounterService { constructor() { if (!CounterService.instance) { this.counter = 0; CounterService.instance = this; } return CounterService.instance; } increment() { this.counter += 1; } decrement() { this.counter -= 1; } getCounter() { return this.counter; } } const instance = new CounterService(); Object.freeze(instance); export default instance;
Now, let’s make a basic React component that uses the CounterService to handle the counter state.
javascript
// src/App.js import React, { useState } from 'react'; import CounterService from './services/CounterService'; const App = () => { const [counter, setCounter] = useState(CounterService.getCounter()); const increment = () => { CounterService.increment(); setCounter(CounterService.getCounter()); }; const decrement = () => { CounterService.decrement(); setCounter(CounterService.getCounter()); }; return ( <div> <h1>Counter: {counter}</h1> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> ); }; export default App;
To test the Singleton behavior, we’ll add another component that interacts with the same CounterService.
javascript
// src/components/CounterDisplay.js import React, { useState, useEffect } from 'react'; import CounterService from '../services/CounterService'; const CounterDisplay = () => { const [counter, setCounter] = useState(CounterService.getCounter()); useEffect(() => { const interval = setInterval(() => { setCounter(CounterService.getCounter()); }, 1000); return () => clearInterval(interval); }, []); return <h2>Counter Display: {counter}</h2>; }; export default CounterDisplay;
Now, update App.js to include the CounterDisplay component:
javascript
Copy code
// src/App.js import React, { useState } from 'react'; import CounterService from './services/CounterService'; import CounterDisplay from './components/CounterDisplay'; const App = () => { const [counter, setCounter] = useState(CounterService.getCounter()); const increment = () => { CounterService.increment(); setCounter(CounterService.getCounter()); }; const decrement = () => { CounterService.decrement(); setCounter(CounterService.getCounter()); }; return ( <div> <h1>Counter: {counter}</h1> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> <CounterDisplay /> </div> ); }; export default App;
What is React Context?
React context enables us to pass down and use (consume) data in any component in our React app without the requirement for props. In other words, the React context makes it easier to transfer data (state) amongst our components.
When should you use React Context?
React context is ideal for passing data that may be utilized in any component of your application.
These kinds of data include:
- Theme data (such as dark or light mode).
- User data (currently authenticated user)
- Location-specific information (such as user language or locale)
Data should be stored in React contexts that do not need to be updated frequently.
Why? Because context was not designed as a comprehensive state management system, it was designed to make data consumption easier.
React context can be thought of as a global variable for our React components.
What difficulties can React context solve?
React context allows us to avoid the problem of prop drilling.
Props drilling is a term used to indicate passing props down multiple levels to a nested component via components that do not require it.
Here’s an example of prop drilling. In our application, we have access to theme data, which we intend to send as a prop to all of our app’s components.
As you can see, App’s direct descendants, such as Header, must also pass the theme data down via props.
export default function App({ theme }) { return ( <> <Header theme={theme} /> <Main theme={theme} /> <Sidebar theme={theme} /> <Footer theme={theme} /> </> ); } function Header({ theme }) { return ( <> <User theme={theme} /> <Login theme={theme} /> <Menu theme={theme} /> </> ); }
What’s the problem with this example? The problem is that we’re passing the theme prop across several components that don’t need it right away.
The Header component does not require a theme other than to send it down to its child components. In other words, it would be preferable if User, Login, and Menu consumed the theme data directly. This is the advantage of React context: we can completely avoid utilizing props and so escape the issue of props drilling.
React singleton vs Context: What are They and Their Differences
-
What is a Singleton?
In software engineering, a singleton is a design pattern that limits the number of instances of a class to one. This ensures that the class only exists once across the application, giving it a global point of access. Singletons are frequently used to manage common resources such as configurations, logging services, and, in the case of React apps, app-wide states.
In React, a singleton can be used to handle global state or common logic across multiple components. This is usually accomplished by generating a single instance of a state management object or class and importing it as needed. Here is a simple example:
javascript
// Singleton.js class Singleton { constructor() { if (!Singleton.instance) { this.state = {}; Singleton.instance = this; } return Singleton.instance; } getState() { return this.state; } setState(newState) { this.state = { ...this.state, ...newState }; } } const instance = new Singleton(); Object.freeze(instance); export default instance;
In the above example, the Singleton class ensures that the state object exists only once. This instance can then be imported and utilized in any component:
javascript
// ComponentA.js import singleton from './Singleton'; singleton.setState({ user: 'John' }); console.log(singleton.getState()); // { user: 'John' } // ComponentB.js import singleton from './Singleton'; console.log(singleton.getState()); // { user: 'John' }
-
What is Context?
The Context API in React is a built-in tool that enables developers to transport data through the component tree without explicitly passing props at each level. It is intended to communicate data that is “global” for a tree of React components, such as the current authorized user, theme, or application settings.
To use the Context API, first construct a context object with React.createContext(), then assign it a value at the top of your component tree, and finally use it in any component that requires access to the shared data.
javascript
// ThemeContext.js import React from 'react'; const ThemeContext = React.createContext('light'); export default ThemeContext; In this example, ThemeContext is created with the default value 'light'. To give this context for components: javascript // App.js import React, { useState } from 'react'; import ThemeContext from './ThemeContext'; import Toolbar from './Toolbar'; function App() { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={theme}> <Toolbar /> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle Theme </button> </ThemeContext.Provider> ); } export default App;
To consume the context in a component:
javascript
// Toolbar.js import React, { useContext } from 'react'; import ThemeContext from './ThemeContext'; function Toolbar() { const theme = useContext(ThemeContext); return <div style={{ background: theme === 'light' ? '#fff' : '#333' }}>Toolbar</div>; } export default Toolbar;
FAQs
What is singleton service?
A singleton service is one that runs on a Managed Server that is only available to one member of the cluster at a time.
What is singleton in React?
The singleton pattern is a software design pattern that limits the instantiation of a class to a “single” instance. This is beneficial when only one object is required to coordinate actions throughout the system.
Is singleton service thread-safe?
A thread-safe singleton is designed such that the singleton property is preserved even in a multithreaded context. To make a singleton class thread safe, the getInstance() method is synchronized so that multiple threads cannot access it concurrently.