diff --git a/ProcessLib/ComponentTransport/ComponentTransportFEM.h b/ProcessLib/ComponentTransport/ComponentTransportFEM.h
index 8b615642ecee3519ac5a1f2533828c103aa8bc09..5a235ee8f2fcd9d84cc44b16fd4d2b13fc3d442d 100644
--- a/ProcessLib/ComponentTransport/ComponentTransportFEM.h
+++ b/ProcessLib/ComponentTransport/ComponentTransportFEM.h
@@ -121,6 +121,54 @@ public:
         setChemicalSystemConcrete(local_x, t, dt);
     }
 
+    void assembleReactionEquation(
+        std::size_t const mesh_item_id,
+        std::vector<NumLib::LocalToGlobalIndexMap const*> const& dof_tables,
+        std::vector<GlobalVector*> const& x, double const t, double const dt,
+        GlobalMatrix& M, GlobalVector& b, int const process_id)
+    {
+        std::vector<double> local_x_vec;
+
+        auto const n_processes = x.size();
+        for (std::size_t pcs_id = 0; pcs_id < n_processes; ++pcs_id)
+        {
+            auto const indices =
+                NumLib::getIndices(mesh_item_id, *dof_tables[pcs_id]);
+            assert(!indices.empty());
+            auto const local_solution = x[pcs_id]->get(indices);
+            local_x_vec.insert(std::end(local_x_vec),
+                               std::begin(local_solution),
+                               std::end(local_solution));
+        }
+        auto const local_x = MathLib::toVector(local_x_vec);
+
+        auto const indices =
+            NumLib::getIndices(mesh_item_id, *dof_tables[process_id]);
+        auto const num_r_c = indices.size();
+
+        std::vector<double> local_M_data;
+        local_M_data.reserve(num_r_c * num_r_c);
+        std::vector<double> local_b_data;
+        local_b_data.reserve(num_r_c);
+
+        assembleReactionEquationConcrete(t, dt, local_x, local_M_data,
+                                         local_b_data, process_id);
+
+        auto const r_c_indices =
+            NumLib::LocalToGlobalIndexMap::RowColumnIndices(indices, indices);
+        if (!local_M_data.empty())
+        {
+            auto const local_M =
+                MathLib::toMatrix(local_M_data, num_r_c, num_r_c);
+            M.add(r_c_indices, local_M);
+        }
+
+        if (!local_b_data.empty())
+        {
+            b.add(indices, local_b_data);
+        }
+    }
+
     virtual std::vector<double> const& getIntPtDarcyVelocity(
         const double t,
         std::vector<GlobalVector*> const& x,
@@ -141,6 +189,11 @@ private:
                                            double const /*t*/,
                                            double const /*dt*/) = 0;
 
+    virtual void assembleReactionEquationConcrete(
+        double const t, double const dt, Eigen::VectorXd const& local_x,
+        std::vector<double>& local_M_data, std::vector<double>& local_b_data,
+        int const transport_process_id) = 0;
+
 protected:
     CoupledSolutionsForStaggeredScheme* _coupled_solutions{nullptr};
 };
@@ -930,6 +983,73 @@ public:
         }
     }
 
+    void assembleReactionEquationConcrete(
+        double const t, double const dt, Eigen::VectorXd const& local_x,
+        std::vector<double>& local_M_data, std::vector<double>& local_b_data,
+        int const transport_process_id) override
+    {
+        auto const local_C = local_x.template segment<concentration_size>(
+            first_concentration_index +
+            (transport_process_id - 1) * concentration_size);
+
+        auto local_M = MathLib::createZeroedMatrix<LocalBlockMatrixType>(
+            local_M_data, concentration_size, concentration_size);
+        auto local_b = MathLib::createZeroedVector<LocalVectorType>(
+            local_b_data, concentration_size);
+
+        unsigned const n_integration_points =
+            _integration_method.getNumberOfPoints();
+
+        ParameterLib::SpatialPosition pos;
+        pos.setElementID(_element.getID());
+
+        MaterialPropertyLib::VariableArray vars;
+        MaterialPropertyLib::VariableArray vars_prev;
+
+        auto const& medium =
+            *_process_data.media_map->getMedium(_element.getID());
+        auto const component_id = transport_process_id - 1;
+        for (unsigned ip(0); ip < n_integration_points; ++ip)
+        {
+            pos.setIntegrationPoint(ip);
+
+            auto& ip_data = _ip_data[ip];
+            auto const& N = ip_data.N;
+            auto const w = ip_data.integration_weight;
+            auto& porosity = ip_data.porosity;
+            auto const& porosity_prev = ip_data.porosity_prev;
+            auto const chemical_system_id = ip_data.chemical_system_id;
+
+            double C_int_pt = 0.0;
+            NumLib::shapeFunctionInterpolate(local_C, N, C_int_pt);
+
+            vars[static_cast<int>(
+                MaterialPropertyLib::Variable::concentration)] = C_int_pt;
+
+            // porosity
+            {
+                vars_prev[static_cast<int>(
+                    MaterialPropertyLib::Variable::porosity)] = porosity_prev;
+
+                porosity =
+                    _process_data.chemically_induced_porosity_change
+                        ? porosity_prev
+                        : medium[MaterialPropertyLib::PropertyType::porosity]
+                              .template value<double>(vars, vars_prev, pos, t,
+                                                      dt);
+            }
+
+            local_M.noalias() += w * N.transpose() * porosity * N;
+
+            auto const C_post_int_pt =
+                _process_data.chemical_solver_interface->getConcentration(
+                    component_id, chemical_system_id);
+
+            local_b.noalias() +=
+                w * N.transpose() * porosity * (C_post_int_pt - C_int_pt) / dt;
+        }
+    }
+
     std::vector<double> const& getIntPtDarcyVelocity(
         const double t,
         std::vector<GlobalVector*> const& x,