Skip to main content

Modern React/GraphQL Components

Modern front-end architecture with React, Redux, and build tools.

Overview​

The APS platform is progressively integrating a modern front-end architecture based on React and modern build tools (Webpack), while maintaining compatibility with legacy code based on jQuery and Knockout.

This hybrid approach allows for a progressive migration to modern technologies without a complete rewrite of the application.

Technical Stack​

Front-end Technologies​

Core Libraries:

  • React 17: Declarative UI library
  • React DOM 17: React rendering in the DOM
  • React Redux 7.2: State management with Redux
  • Redux Saga 1.1: Asynchronous side effect management
  • Redux Toolkit 1.7: Modern Redux tools

Styling Libraries:

  • styled-components 5.3: CSS-in-JS
  • polished 4.1: Style utilities
  • classnames 2.3: Conditional CSS class management

Utilities:

  • prop-types 15.8: React props validation
  • use-debounce 7.0: Debounce hook
  • react-content-loader 6.2: Loading skeletons

Legacy Technologies​

The application still uses:

  • jQuery 3.6: DOM manipulation and AJAX
  • Knockout 3.5: MVVM data-binding
  • jQuery UI 1.12: UI components
  • SignalR 2.4: Real-time communication
  • DevExpress: Grid controls and advanced components

Build Tools​

Webpack 5:

  • JavaScript/React module bundling
  • Minification and optimization
  • Source maps for debugging

Babel 7:

  • ES6+ β†’ ES5 transpilation
  • JSX support (React)
  • Polyfills for browser compatibility

Gulp 4:

  • SASS β†’ CSS compilation
  • File concatenation
  • Watch mode for development

TypeScript 4.7:

  • Typed build scripts
  • Administration tools

React Architecture​

Component Organization​

React components are organized in:

Web/Model/Scripts/form/
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ components/ # Reusable React components
β”‚ β”œβ”€β”€ containers/ # Redux-connected components
β”‚ β”œβ”€β”€ store/ # Redux configuration
β”‚ β”œβ”€β”€ sagas/ # Redux Sagas
β”‚ └── utils/ # Utilities
└── webpack.config.js # Webpack configuration

Redux Pattern​

Application state is managed with Redux via Redux Toolkit:

Store:

import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware();

export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(sagaMiddleware),
});

sagaMiddleware.run(rootSaga);

Redux Toolkit Slices:

import { createSlice } from '@reduxjs/toolkit';

const documentSlice = createSlice({
name: 'document',
initialState: {
currentDocument: null,
loading: false,
error: null,
},
reducers: {
loadDocument: (state, action) => {
state.loading = true;
},
loadDocumentSuccess: (state, action) => {
state.currentDocument = action.payload;
state.loading = false;
},
loadDocumentFailure: (state, action) => {
state.error = action.payload;
state.loading = false;
},
},
});

export const { loadDocument, loadDocumentSuccess, loadDocumentFailure }
= documentSlice.actions;
export default documentSlice.reducer;

Redux Saga for Asynchronous Effects​

Asynchronous operations (API calls) are handled via Redux Saga:

import { call, put, takeLatest } from 'redux-saga/effects';
import { loadDocument, loadDocumentSuccess, loadDocumentFailure } from './documentSlice';

function* fetchDocumentSaga(action) {
try {
const response = yield call(fetch, `/api/documents/${action.payload}`);
const data = yield call([response, 'json']);
yield put(loadDocumentSuccess(data));
} catch (error) {
yield put(loadDocumentFailure(error.message));
}
}

export function* watchDocuments() {
yield takeLatest(loadDocument.type, fetchDocumentSaga);
}

Styled Components​

Styles are defined with styled-components for encapsulation:

import styled from 'styled-components';
import { lighten } from 'polished';

export const Button = styled.button`
background-color: ${props => props.theme.primary};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;

&:hover {
background-color: ${props => lighten(0.1, props.theme.primary)};
}

&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;

Build and Webpack​

Webpack Configuration​

The webpack.config.js file configures compilation:

const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
entry: {
form: ['react-app-polyfill/ie11', 'react-app-polyfill/stable', './src/index.jsx'],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
optimization: {
minimizer: [new TerserPlugin()],
},
resolve: {
extensions: ['.js', '.jsx'],
},
};

npm Scripts​

Build scripts are defined in package.json:

{
"scripts": {
"webpack": "cross-env NODE_ENV=production webpack",
"webpack:watch": "cross-env NODE_ENV=development webpack watch",
"gulp": "gulp",
"gulp:watch": "gulp watch",
"build": "npm run gulp && npm run webpack"
}
}

Usage:

# Production build
npm run webpack

# Watch mode (development)
npm run webpack:watch

# Complete build (CSS + JS)
npm run build

Integration with ASP.NET​

Loading Bundles​

Webpack bundles are loaded in ASP.NET views:

@* ASP.NET MVC Page *@
<div id="react-root"></div>

<script src="~/Scripts/form/dist/form.bundle.js"></script>
<script>
// React initialization
FormApp.init({
documentId: '@Model.DocumentId',
formId: '@Model.FormId'
});
</script>

JavaScript ↔ C# Communication​

Data is exchanged via:

Initial Data (C# β†’ JS):

<script>
window.__INITIAL_STATE__ = @Html.Raw(Json.Encode(Model));
</script>

API Calls (JS β†’ C#):

// Call to an ASP.NET endpoint
const response = await fetch('/api/documents/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(documentData),
});

Polyfills and Compatibility​

IE11 Support​

The application uses polyfills to support Internet Explorer 11:

import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import 'core-js/stable';

Polyfill Packages:

  • react-app-polyfill: React polyfills for IE11
  • core-js: ES6+ polyfills
  • @babel/polyfill: Babel polyfills

Hybrid Approach​

Progressive Migration​

The architecture allows coexistence of:

Modern React Code:

  • New components in React
  • State management with Redux
  • Styled components

Legacy Code:

  • Existing jQuery components
  • Knockout data-binding
  • DevExpress controls

Interoperability​

React components can interact with legacy code:

// React component calling jQuery code
import { useEffect } from 'react';

function LegacyComponent({ elementId }) {
useEffect(() => {
// Initialize a jQuery component
$(`#${elementId}`).datepicker({
dateFormat: 'dd/mm/yy'
});

return () => {
// Cleanup
$(`#${elementId}`).datepicker('destroy');
};
}, [elementId]);

return <div id={elementId}></div>;
}

Development Tools​

ESLint​

ESLint configuration for code quality:

{
"extends": [
"airbnb",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:redux-saga/recommended",
"prettier"
],
"plugins": ["react", "react-hooks", "redux-saga"],
"rules": {
"react/prop-types": "warn",
"react-hooks/rules-of-hooks": "error",
"redux-saga/no-unhandled-errors": "error"
}
}

Prettier​

Automatic code formatting:

{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"semi": true
}

Jest​

JavaScript unit tests:

{
"scripts": {
"test:js": "jest"
}
}

GraphQL (Future)​

⚠️ Note: GraphQL is not yet implemented in the current architecture, but could be added in the future to replace REST APIs.

Potential Benefits:

  • Flexible client-side queries
  • Strong typing with GraphQL schemas
  • Reduction of over-fetching
  • Automatic introspection

Envisioned Stack:

  • Apollo Client: React GraphQL client
  • HotChocolate: .NET GraphQL server
  • GraphQL Code Generator: TypeScript generation

Best Practices​

Component Organization​

Functional Components with Hooks:

import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';

function DocumentEditor({ documentId }) {
const dispatch = useDispatch();
const document = useSelector(state => state.documents.current);
const [isDirty, setIsDirty] = useState(false);

useEffect(() => {
dispatch(loadDocument(documentId));
}, [documentId, dispatch]);

// ...
}

Performance​

Memoization:

import { useMemo, useCallback } from 'react';

function ExpensiveComponent({ items }) {
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);

const handleClick = useCallback(
(id) => console.log('Clicked:', id),
[]
);

// ...
}

Code Splitting:

import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}

Future Evolution​

Technical Roadmap​

Short Term:

  • Migration of jQuery components β†’ React
  • React component unit tests
  • Webpack bundle optimization

Medium Term:

  • TypeScript adoption for React
  • GraphQL introduction
  • Migration to React 18

Long Term:

  • 100% React application
  • Server-Side Rendering (SSR)
  • Microfrontends

References​