I'm migrating an existing React app over to Angular. Each React component will eventually be rewritten, but in the meantime I'm hosting them in Angular components based on this method: https://medium.com/@joseph5/rendering-react-component-in-angular-apps-9154fa3198d1. I'm using a component instead of a directive, but otherwise it's the same.
This works fine, and I can render the React components in Angular. What I'd like to do is give them access to services provided in the Angular app. Most importantly, I want them to share the Angular app's NGRX store. The existing components were using useDispatch
and useSelector
from the react-redux library, but since they're now standalone components, there's no app-level store provided for those to use. I have to provide access to my NGRX store, which can't be injected into the React component because it's not in an Angular injectionContext.
What I've been doing is injecting the store into the Angular host and passing it through the props:
// Angular
@Component({
selector: 'app-search-results',
template: `<ng-react [component]="SearchResults" [props]="props"></ng-react>`,
encapsulation: ViewEncapsulation.None,
styleUrl: './search-results.component.scss'
})
export class SearchResultsComponent {
SearchResults = SearchResults;
props = {
store: inject(Store),
};
}
And then in the React counterpart:
// React
const SearchResults = (props) => {
const store = props.store;
const [filterData, setFilterData] = useState(initialState);
// creates an observable once and subscribes to it with the given callback
useObservable(() => store.select((state) => state.filterData), setFilterData);
// ...
This works. The only problem with it is that sometimes these components have children and grandchildren that also need access to NGRX or some other Angular service. I'd prefer not to keep passing it down through the props. I've experimented with createContext
, but I can't find a satisfying way to use it for this. Can anyone recommend the best way to share my NGRX store with these React components?
I found a much cleaner solution by using React's Context feature. The Angular host injects any dependencies as normal, then drops them in an object which it passes to my ng-react
adapter, which wraps and renders the React component inside a Context. There's no need to pass all the dependencies down through the props
input from parent to child; all the components in the tree can directly touch the injected dependencies through React's useContext
function. I abstracted almost all of this away, so it all feels seamless at the component level.
There are several pieces to the full solution:
angular-context.ts
: Sets up a Context for my ng-react
adapter, then gives the React components easy access to it.
export const AngularContext = createContext<IContextType>({});
export function getAngularContext() {
return useContext(AngularContext);
}
ng-react-adapter.component.ts
: A generic adapter that receives a React component class and renders it inside a React Context. This is an Angular component.
@Input() component!: Comp;
@Input() props?: ComponentProps<Comp>;
@Input() context?: { [key: string]: any };
private root = createRoot(inject(ElementRef).nativeElement);
ngOnChanges() {
this.root.render(
// We render our React component inside a context that can provide injected services from Angular.
createElement(
AngularContext.Provider,
{ value: this.context },
createElement(this.component, this.props)
)
);
}
ngOnDestroy() {
this.root.unmount();
}
Inside the Angular host component:
@Component({
selector: 'app-admin',
template: `<ng-react [component]="Admin" [context]="context"></ng-react>`,
encapsulation: ViewEncapsulation.None,
styleUrl: './admin.component.scss',
})
export class AdminComponent {
Admin = Admin;
context = {
store: inject(Store),
};
}
Now my helper for convenient access to the injected NGRX store:
export function getStore(): Store<IStoreState> {
const context = getAngularContext();
let store: Store<IStoreState>;
try {
store = context.store;
} catch {}
if (!store) {
throw "No NGRX store found! Make sure you inject it into the [context] input of the ng-react adapter.";
}
return store;
}
All this makes everything simple for the great-great grandchild of Admin.tsx, which doesn't even have to touch the Context, and none of its parents have to know about it:
export const MyChildComponent = (props) => {
const store = getStore();
React.useEffect(() => {
store.dispatch(dialogActions.GetStateList());
}, []);